{
  "openapi": "3.1.0",
  "info": {
    "title": "Tokenistry Investment Reconciliation",
    "description": "Reconstructs effective investment ownership from token transfers indexed by the Tokenistry DLT indexing engine.\n\nThe blockchain says where tokens moved. Investor mappings say whether economic ownership moved. Unless explicit mapping puts both wallets under the same investor, a wallet-to-wallet transfer is an ownership transfer.\n\nAll amounts are raw integer token units rendered as decimal strings. Decimals are presentation metadata and are never applied here.",
    "license": {
      "name": "LicenseRef-Tokenistry-Commercial",
      "identifier": "LicenseRef-Tokenistry-Commercial"
    },
    "version": "1.0.0"
  },
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "system"
        ],
        "summary": "Liveness probe.",
        "operationId": "health",
        "responses": {
          "200": {
            "description": "The service is running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets": {
      "get": {
        "tags": [
          "assets"
        ],
        "operationId": "list_assets",
        "responses": {
          "200": {
            "description": "All registered assets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AssetResponse"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Register a token to reconcile.",
        "description": "The indexer ids say where its transfers come from; this service never\ncontacts a node itself. Registering the same `(chain_id, token_address)`\ntwice updates the existing asset rather than duplicating it.",
        "operationId": "create_asset",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAssetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "The asset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssetResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/anomalies": {
      "get": {
        "tags": [
          "events"
        ],
        "operationId": "list_anomalies",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recorded anomalies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AnomalyResponse"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/events": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "Reconciled events in blockchain order. Filter by `kind` to get just the\nownership transfers, reallocations, mints or burns.",
        "operationId": "list_events",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "description": "`mint`, `burn`, `reallocation` or `ownership_transfer`.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wallet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "investor_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "from_block",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 0
            }
          },
          {
            "name": "to_block",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 0
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Keyset pagination: return events strictly after `block-txIndex-logIndex`.",
            "required": false,
            "schema": {
              "type": "string"
            },
            "example": "18000000-4-7"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1..=1000, defaults to 100.",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ReconciledEventResponse"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid filter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/holders": {
      "get": {
        "tags": [
          "positions"
        ],
        "summary": "Everyone still holding units of an asset, largest holding first.",
        "description": "Unmapped wallets are listed as their own owners: that is what they are to\nthe ledger, and leaving them out would stop the rows adding up to the\nreconciled supply.",
        "operationId": "list_holders",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current holders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/HolderResponse"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/investments": {
      "get": {
        "tags": [
          "positions"
        ],
        "summary": "Every acquisition an owner made in an asset, newest first.",
        "description": "An investment is the units received in one transaction and whatever FIFO has\nsince left of them, wherever the owner's own reallocations have since put\nthem.",
        "operationId": "list_investments",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "investor_id",
            "in": "query",
            "description": "The investor whose investments to list. Exactly one of this and\n`wallet` is required.",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "wallet",
            "in": "query",
            "description": "An unmapped wallet, which owns its own investments.",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "open_only",
            "in": "query",
            "description": "Only acquisitions that still hold units. Defaults to true.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The owner's investments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/InvestmentResponse"
                  }
                }
              }
            }
          },
          "400": {
            "description": "No owner, or both",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/investments/{seq}/transfers": {
      "get": {
        "tags": [
          "positions"
        ],
        "summary": "Every transfer that touched one investment, oldest first.",
        "description": "`units_in` and `units_out` are the investment's own share of each transfer,\nwhich is not the transfer's amount whenever it also moved units belonging to\nanother acquisition. Both non-zero is a reallocation between the owner's own\nwallets.",
        "operationId": "investment_transfers",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "seq",
            "in": "path",
            "description": "The investment's id: its first lot",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Transfers that touched it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/InvestmentTransferResponse"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such lot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/lots": {
      "get": {
        "tags": [
          "lots"
        ],
        "operationId": "list_lots",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "wallet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "investor_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "active_only",
            "in": "query",
            "description": "Only lots that still hold units. Defaults to true.",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1..=1000, defaults to 100.",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching lots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LotResponse"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/lots/{seq}/history": {
      "get": {
        "tags": [
          "lots"
        ],
        "summary": "Every reconciled event that took units out of or put units into a lot.",
        "operationId": "lot_history",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "seq",
            "in": "path",
            "description": "Lot sequence number",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Events touching the lot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ReconciledEventResponse"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/lots/{seq}/provenance": {
      "get": {
        "tags": [
          "lots"
        ],
        "summary": "A lot and every lot it descends from, nearest ancestor first, ending at the\nmint. This answers where a position came from and which ownership transfers\nit passed through.",
        "operationId": "lot_provenance",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "seq",
            "in": "path",
            "description": "Lot sequence number",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The provenance chain",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LotResponse"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such lot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/positions/investors/{investor_id}": {
      "get": {
        "tags": [
          "positions"
        ],
        "summary": "What an investor economically owns of an asset, broken down by the wallets\nholding it.",
        "operationId": "investor_position",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "investor_id",
            "in": "path",
            "description": "Investor id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The investor's position",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PositionResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/positions/wallets/{wallet}": {
      "get": {
        "tags": [
          "positions"
        ],
        "summary": "What a wallet holds, split by who owns it. More than one row means the\nwallet changed investor while still holding lots acquired under the earlier\nmapping.",
        "operationId": "wallet_position",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "wallet",
            "in": "path",
            "description": "0x-prefixed wallet address",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The wallet's position",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PositionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/reconcile": {
      "post": {
        "tags": [
          "reconciliation"
        ],
        "summary": "Pull whatever the indexer has that this asset has not reconciled yet.",
        "description": "The background worker does this on a timer and whenever the indexer's live\nstream says there is something new. This endpoint is for forcing the issue.",
        "operationId": "reconcile",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What the catch-up did",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplayResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such asset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "502": {
            "description": "The indexer could not be read",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/replay": {
      "post": {
        "tags": [
          "reconciliation"
        ],
        "summary": "Rebuild an asset's reconciled state from the indexed history.",
        "description": "Safe to call at any time and safe to call twice: the same history and the\nsame assignments always produce the same result.",
        "operationId": "replay",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReplayRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The replay finished",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplayResponse"
                }
              }
            }
          },
          "202": {
            "description": "The replay was started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReplayResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such asset",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assets/{asset_id}/status": {
      "get": {
        "tags": [
          "reconciliation"
        ],
        "summary": "How far reconciliation has got for an asset, and how the last replay went.",
        "operationId": "status",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "description": "Asset id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reconciliation status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assignments": {
      "get": {
        "tags": [
          "assignments"
        ],
        "operationId": "list_assignments",
        "parameters": [
          {
            "name": "investor_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "chain_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 0
            }
          },
          {
            "name": "wallet",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching assignments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AssignmentResponse"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "assignments"
        ],
        "summary": "Assign a wallet to an investor.",
        "description": "Until this exists, a transfer to or from the wallet is an ownership transfer,\nbecause\nthe engine will not guess that two wallets share an owner. Assigning both\nends of a past transfer and replaying turns it into a reallocation.",
        "operationId": "create_assignment",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAssignmentRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "The assignment and what it invalidated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignmentChangeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "409": {
            "description": "The wallet is already claimed over those blocks",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assignments/{id}": {
      "delete": {
        "tags": [
          "assignments"
        ],
        "summary": "Delete an assignment that should never have existed. The wallet reverts to\nowning itself, so its transfers become ownership transfers again after a\nreplay.",
        "operationId": "delete_assignment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Assignment id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The removed assignment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignmentChangeResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such assignment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assignments/{id}/close": {
      "post": {
        "tags": [
          "assignments"
        ],
        "summary": "End an assignment at a block, keeping the earlier period intact. Use this\nwhen a wallet genuinely changed hands.",
        "operationId": "close_assignment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Assignment id",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CloseAssignmentRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The closed assignment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignmentChangeResponse"
                }
              }
            }
          },
          "404": {
            "description": "No such open assignment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/catalog/sync": {
      "post": {
        "tags": [
          "assets"
        ],
        "summary": "Replace the local catalog with the indexer's Transfer subscriptions.",
        "description": "Tokens this engine has never been told about by the indexer — including\nleftovers from integration tests — are deleted. The background worker does\nthis on its own; the endpoint is here so an operator does not have to wait.",
        "operationId": "sync_catalog",
        "responses": {
          "200": {
            "description": "What changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogSyncResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/investors": {
      "get": {
        "tags": [
          "investors"
        ],
        "operationId": "list_investors",
        "responses": {
          "200": {
            "description": "All investors",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/InvestorResponse"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "investors"
        ],
        "summary": "Register an investor, or return the existing one with the same external id.",
        "operationId": "create_investor",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvestorRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "The investor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvestorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      }
    },
    "/v1/metrics": {
      "get": {
        "tags": [
          "system"
        ],
        "summary": "Counters for throughput, classification mix and how far behind the indexer\nreconciliation is running.",
        "operationId": "metrics",
        "responses": {
          "200": {
            "description": "Process counters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AnomalyResponse": {
        "type": "object",
        "required": [
          "id",
          "block_number",
          "kind",
          "detail"
        ],
        "properties": {
          "block_number": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "detail": {
            "type": "string"
          },
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "kind": {
            "type": "string"
          }
        }
      },
      "AssetResponse": {
        "type": "object",
        "required": [
          "id",
          "chain_id",
          "token_address",
          "indexer_network_id",
          "indexer_contract_id",
          "indexer_event_name",
          "start_block",
          "enabled"
        ],
        "properties": {
          "chain_id": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "decimals": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32"
          },
          "enabled": {
            "type": "boolean"
          },
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "indexer_contract_id": {
            "type": "integer",
            "format": "int64"
          },
          "indexer_event_name": {
            "type": "string"
          },
          "indexer_network_id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "start_block": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "symbol": {
            "type": [
              "string",
              "null"
            ]
          },
          "token_address": {
            "type": "string"
          }
        }
      },
      "AssignmentChangeResponse": {
        "type": "object",
        "required": [
          "assignment",
          "affected_assets",
          "replay_started"
        ],
        "properties": {
          "affected_assets": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Assets whose reconciled history involves this wallet. Their positions\nare stale until these are replayed."
          },
          "assignment": {
            "$ref": "#/components/schemas/AssignmentResponse"
          },
          "replay_started": {
            "type": "boolean",
            "description": "Whether a replay of those assets was started by this request."
          }
        }
      },
      "AssignmentResponse": {
        "type": "object",
        "required": [
          "id",
          "chain_id",
          "wallet",
          "investor_id",
          "valid_from_block"
        ],
        "properties": {
          "chain_id": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "investor_id": {
            "type": "integer",
            "format": "int64"
          },
          "valid_from_block": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "valid_to_block": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "wallet": {
            "type": "string"
          }
        }
      },
      "CatalogSyncResponse": {
        "type": "object",
        "required": [
          "upserted",
          "removed"
        ],
        "properties": {
          "removed": {
            "type": "integer",
            "format": "int64",
            "description": "Local assets that were not in the indexer and have been dropped."
          },
          "upserted": {
            "type": "integer",
            "format": "int64",
            "description": "Tokens the indexer is subscribed to for `Transfer`, now in the catalog."
          }
        }
      },
      "CloseAssignmentRequest": {
        "type": "object",
        "required": [
          "at_block"
        ],
        "properties": {
          "at_block": {
            "type": "integer",
            "format": "int64",
            "description": "The assignment stops applying at this block.",
            "minimum": 0
          },
          "replay": {
            "type": [
              "boolean",
              "null"
            ]
          }
        }
      },
      "CreateAssetRequest": {
        "type": "object",
        "required": [
          "chain_id",
          "token_address",
          "indexer_network_id",
          "indexer_contract_id"
        ],
        "properties": {
          "chain_id": {
            "type": "integer",
            "format": "int64",
            "example": 137,
            "minimum": 0
          },
          "decimals": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "Presentation metadata only. No accounting path reads it."
          },
          "indexer_contract_id": {
            "type": "integer",
            "format": "int64",
            "description": "The indexer's contract id for this token."
          },
          "indexer_event_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Defaults to `Transfer`."
          },
          "indexer_network_id": {
            "type": "integer",
            "format": "int64",
            "description": "The indexer's network id for this chain."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "ERC-20 `name()` from the indexer, when known."
          },
          "start_block": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Reconcile from this block onwards. Defaults to 0.",
            "minimum": 0
          },
          "symbol": {
            "type": [
              "string",
              "null"
            ]
          },
          "token_address": {
            "type": "string",
            "example": "0x20dd5ddb99b97141f624450debd781bb0d1c6d71"
          }
        }
      },
      "CreateAssignmentRequest": {
        "type": "object",
        "required": [
          "chain_id",
          "wallet",
          "investor_id"
        ],
        "properties": {
          "chain_id": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "investor_id": {
            "type": "integer",
            "format": "int64"
          },
          "replay": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Replay the assets this wallet appears in, so positions reflect the new\nmapping immediately. Defaults to true."
          },
          "valid_from_block": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The assignment applies from this block onwards. Defaults to 0, i.e. for\nthe whole of history.",
            "minimum": 0
          },
          "valid_to_block": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Exclusive upper bound. Omit to leave the assignment open.",
            "minimum": 0
          },
          "wallet": {
            "type": "string",
            "example": "0x00000000000000000000000000000000000000a1"
          }
        }
      },
      "CreateInvestorRequest": {
        "type": "object",
        "required": [
          "external_id"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "description": "The investor's identity in the calling system. Re-registering the same\nid returns the same investor rather than creating a second one.",
            "example": "investor-42"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ErrorBody": {
        "type": "object",
        "description": "Machine readable error envelope, identical for every failing endpoint.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorDetail"
          }
        }
      },
      "ErrorDetail": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable, snake_case error code.",
            "example": "not_found"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "status",
          "version"
        ],
        "properties": {
          "status": {
            "type": "string"
          },
          "version": {
            "type": "string"
          }
        }
      },
      "HolderResponse": {
        "type": "object",
        "required": [
          "amount",
          "lots",
          "wallets",
          "investments"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "example": "1000000000000000000"
          },
          "investments": {
            "type": "integer",
            "format": "int64",
            "description": "Distinct acquisitions still open."
          },
          "investor_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "`null` for an unmapped wallet, which stands in as its own economic\nowner. Exactly one of this and `wallet` is set."
          },
          "lots": {
            "type": "integer",
            "format": "int64",
            "description": "Open lots behind the holding."
          },
          "wallet": {
            "type": [
              "string",
              "null"
            ]
          },
          "wallets": {
            "type": "integer",
            "format": "int64",
            "description": "Wallets the holding is spread across."
          }
        }
      },
      "HoldingResponse": {
        "type": "object",
        "required": [
          "wallet",
          "amount",
          "lots"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "example": "1000000000000000000"
          },
          "investor_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "`null` means the wallet has no investor mapping and is therefore its own\neconomic owner."
          },
          "lots": {
            "type": "integer",
            "format": "int64"
          },
          "wallet": {
            "type": "string"
          }
        }
      },
      "InvestmentResponse": {
        "type": "object",
        "description": "One acquisition by one owner, and what FIFO has left of it.",
        "required": [
          "seq",
          "acquired_block",
          "acquired_transaction",
          "acquired_from",
          "acquired_amount",
          "remaining",
          "released",
          "entry_lots",
          "lots",
          "roots",
          "unbacked",
          "wallets"
        ],
        "properties": {
          "acquired_amount": {
            "type": "string",
            "description": "Units received at the acquisition."
          },
          "acquired_block": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "acquired_from": {
            "type": "string",
            "description": "`mint`, `unbacked`, or `ownership_transfer` when it came from another\nowner."
          },
          "acquired_transaction": {
            "type": "string"
          },
          "entry_lots": {
            "type": "integer",
            "format": "int64",
            "description": "Parcels the acquisition arrived as: one per source lot it consumed."
          },
          "from_investor_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The investor it was received from, when that wallet was mapped at the\ntime."
          },
          "from_wallet": {
            "type": [
              "string",
              "null"
            ],
            "description": "The wallet it was received from. Absent for a mint."
          },
          "lots": {
            "type": "integer",
            "format": "int64",
            "description": "Parcels it sits in now, reallocations included."
          },
          "released": {
            "type": "string",
            "description": "Units transferred on or burnt since. `acquired_amount - remaining`."
          },
          "remaining": {
            "type": "string",
            "description": "Units still held, wherever they now sit."
          },
          "roots": {
            "type": "integer",
            "format": "int64",
            "description": "Distinct mints at the root of it."
          },
          "seq": {
            "type": "integer",
            "format": "int64",
            "description": "The first lot the acquisition produced. Stable across replays, and the\nid to pass to `/investments/{seq}/transfers`."
          },
          "unbacked": {
            "type": "boolean",
            "description": "Whether any of it descends from an `unbacked` root rather than a mint."
          },
          "wallets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WalletSplitResponse"
            },
            "description": "Where the remaining units sit, largest first."
          }
        }
      },
      "InvestmentTransferResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ReconciledEventResponse"
          },
          {
            "type": "object",
            "required": [
              "units_in",
              "units_out"
            ],
            "properties": {
              "units_in": {
                "type": "string"
              },
              "units_out": {
                "type": "string"
              }
            }
          }
        ],
        "description": "A transfer seen from one investment. `units_in` and `units_out` both\nnon-zero means a reallocation between the owner's own wallets."
      },
      "InvestorResponse": {
        "type": "object",
        "required": [
          "id",
          "external_id"
        ],
        "properties": {
          "external_id": {
            "type": "string"
          },
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "LotResponse": {
        "type": "object",
        "required": [
          "seq",
          "wallet",
          "original_amount",
          "remaining",
          "root_seq",
          "origin",
          "origin_block",
          "origin_transaction",
          "acquired_block",
          "acquired_transaction"
        ],
        "properties": {
          "acquired_block": {
            "type": "integer",
            "format": "int64",
            "description": "When the current owner acquired the units. Unchanged by a reallocation.",
            "minimum": 0
          },
          "acquired_transaction": {
            "type": "string"
          },
          "investor_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "origin": {
            "type": "string",
            "description": "`mint`, or `unbacked` for units the indexed history could not account\nfor."
          },
          "origin_block": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "origin_transaction": {
            "type": "string"
          },
          "original_amount": {
            "type": "string"
          },
          "parent_seq": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "The lot this one was split from."
          },
          "remaining": {
            "type": "string"
          },
          "root_seq": {
            "type": "integer",
            "format": "int64",
            "description": "The mint at the root of this lot's provenance chain."
          },
          "seq": {
            "type": "integer",
            "format": "int64"
          },
          "wallet": {
            "type": "string"
          }
        }
      },
      "PositionResponse": {
        "type": "object",
        "required": [
          "asset_id",
          "total",
          "holdings"
        ],
        "properties": {
          "asset_id": {
            "type": "integer",
            "format": "int64"
          },
          "holdings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HoldingResponse"
            }
          },
          "total": {
            "type": "string"
          }
        }
      },
      "ReconciledEventResponse": {
        "type": "object",
        "required": [
          "id",
          "block_number",
          "transaction_index",
          "log_index",
          "block_hash",
          "transaction_hash",
          "kind",
          "amount"
        ],
        "properties": {
          "amount": {
            "type": "string"
          },
          "block_hash": {
            "type": "string"
          },
          "block_number": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "from_investor_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "from_wallet": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "kind": {
            "type": "string",
            "description": "`mint`, `burn`, `reallocation` or `ownership_transfer`.",
            "example": "ownership_transfer"
          },
          "log_index": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "to_investor_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "to_wallet": {
            "type": [
              "string",
              "null"
            ]
          },
          "transaction_hash": {
            "type": "string"
          },
          "transaction_index": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "ReplayRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Recorded against the replay run for audit.",
            "example": "investor mapping corrected"
          },
          "wait": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Block until the replay finishes instead of returning immediately.\nConvenient for small tokens and for tests; a long history should be\nstarted asynchronously and followed with `GET /assets/{id}/status`."
          }
        }
      },
      "ReplayResponse": {
        "type": "object",
        "required": [
          "asset_id",
          "started",
          "finished"
        ],
        "properties": {
          "asset_id": {
            "type": "integer",
            "format": "int64"
          },
          "duration_millis": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "events_processed": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "finished": {
            "type": "boolean"
          },
          "started": {
            "type": "boolean"
          }
        }
      },
      "ReplayRunResponse": {
        "type": "object",
        "required": [
          "id",
          "reason",
          "status",
          "events_processed",
          "started_at"
        ],
        "properties": {
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "events_processed": {
            "type": "integer",
            "format": "int64"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "reason": {
            "type": "string"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "StatusResponse": {
        "type": "object",
        "required": [
          "asset_id",
          "events_reconciled",
          "active_lots",
          "total_active_units",
          "anomalies"
        ],
        "properties": {
          "active_lots": {
            "type": "integer",
            "format": "int64"
          },
          "anomalies": {
            "type": "integer",
            "format": "int64"
          },
          "asset_id": {
            "type": "integer",
            "format": "int64"
          },
          "events_reconciled": {
            "type": "integer",
            "format": "int64"
          },
          "last_replay": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ReplayRunResponse"
              }
            ]
          },
          "reconciled_block": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 0
          },
          "reconciled_position": {
            "type": [
              "string",
              "null"
            ],
            "description": "The last position reconciled, as `block-txIndex-logIndex`."
          },
          "total_active_units": {
            "type": "string"
          }
        }
      },
      "WalletSplitResponse": {
        "type": "object",
        "required": [
          "wallet",
          "amount",
          "lots"
        ],
        "properties": {
          "amount": {
            "type": "string"
          },
          "lots": {
            "type": "integer",
            "format": "int64"
          },
          "wallet": {
            "type": "string"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "investors",
      "description": "Investor identities"
    },
    {
      "name": "assets",
      "description": "Tokens to reconcile"
    },
    {
      "name": "assignments",
      "description": "Which wallets belong to which investor"
    },
    {
      "name": "positions",
      "description": "Current ownership"
    },
    {
      "name": "lots",
      "description": "FIFO lots and provenance"
    },
    {
      "name": "events",
      "description": "Classified transfers"
    },
    {
      "name": "reconciliation",
      "description": "Status, catch-up and replay"
    },
    {
      "name": "system",
      "description": "Health and metrics"
    }
  ]
}
