openapi: 3.0.0
info:
  title: Trustblock Public API
  version: 1.1.0
  description: |
    The public routes of the Trustblock API, as they behave today.

    This document is a description, not a promise. It covers only the `/v1` routes that are available
    to callers outside Trustblock; internal routes used by the website and by administrators are
    deliberately absent. Where current behaviour is inconsistent and under review, the route says so
    rather than picking the answer we would prefer.

    Every route requires a bearer token. There is no unauthenticated route.
  contact:
    name: Trustblock documentation
    url: https://docs.trustblock.run
servers:
  - url: https://api.trustblock.run
    description: Production. This is the only server.
tags:
  - name: Project security
    description: Read the published audit state of a project.
  - name: Cards
    description: Compact summaries for third-party block explorers.
  - name: Publishing
    description: Routes for audit providers to publish and correct their own audits.
paths:
  /v1/project/security:
    get:
      tags: [Project security]
      operationId: getProjectSecurityState
      summary: Get a project's security state
      description: |
        Returns the audits published for a project, the unresolved issues of its most recent audit,
        and any recorded incidents.

        Identify the project by exactly one of `domain`, `slug`, or the pair `chain` and `address`.
        Supplying `chain` without `address`, or `address` without `chain`, is rejected.

        **Consumes one API credit**, and only when a project is returned. A request that resolves to
        nothing is not charged.

        **A limitation to design around: two different facts produce one identical response.** A project
        Trustblock has never heard of, and a project Trustblock knows about that has no published audit,
        both answer `404` with `code: "ProjectNotFound"` and the same message. Nothing in the response
        distinguishes them. A caller cannot use this route to tell "unknown project" from "no audit yet",
        and most projects in the registry are in the second case rather than the first.

        **Behaviour under review:** the two card routes answer `200` with `isFound: false` for that same
        condition, where this route answers `404`. The two are inconsistent today and the inconsistency
        has not been resolved in either direction, so neither shape should be treated as settled.
      security:
        - BearerAuth: []
      parameters:
        - in: query
          name: domain
          required: false
          schema: { type: string }
          description: The project's domain, for example `example.com`.
          example: uniswap.org
        - in: query
          name: slug
          required: false
          schema: { type: string }
          description: The project's Trustblock slug.
        - in: query
          name: chain
          required: false
          schema: { $ref: '#/components/schemas/Chain' }
          description: Chain of a contract belonging to the project. Must be sent with `address`.
        - in: query
          name: address
          required: false
          schema: { type: string }
          description: Address of a contract belonging to the project. Must be sent with `chain`.
      responses:
        '200':
          description: The project's security state.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GetProjectSecurityResponse' }
        '400':
          description: The query is unusable - for example `chain` without `address`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          description: No `Authorization` header was sent.
        '402':
          description: The caller's API credits are exhausted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '403':
          description: The bearer token is not a recognised key.
        '404':
          description: |
            No project matched, **or** the project exists and has no published audit. These two cases
            are indistinguishable in the response. See the note above.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /v1/project/etherscan-card:
    get:
      tags: [Cards]
      operationId: getProjectEtherscanCard
      summary: Get a project's Etherscan card
      description: |
        A rendered summary of a project's audit state, shaped for the Etherscan contract page:
        display strings, an image and a status colour.

        Identified the same way as `/v1/project/security`, and subject to the same `chain`/`address`
        pairing rule. **This route consumes no API credit.**

        **The five cards.** The worst state wins, in this order:

        | State | `centerText` | `statusColor` | `isFound` / `status` |
        |---|---|---|---|
        | Incident | "Incident 24 May 2026" | `#E84353` red | `true` |
        | Open critical | "2 open critical findings" | `#E84353` red | `true` |
        | Acknowledged critical | "1 critical acknowledged, not fixed" | `#F08D3C` orange | `true` |
        | None open | "No open critical findings" | `#67A22F` green | `true` |
        | No audit | "No audit on Trustblock" | `#F08D3C` orange | `false` |

        "Open" means neither fixed nor acknowledged, across every audit still in force. An incident is
        shown while it is later than the latest audit in force, or less than twelve months old.

        **The contract line.** When the request names a contract (`chain` + `address`) and an audit's own
        scope names that address, the card carries `contractLine`, for example "This address was in the
        scope of the Hacken audit of 9 Sep 2026". Otherwise the field is absent and the card stays
        project-level.

        **Caching.** A lookup by `chain` + `address` alone may be answered from a cache for up to five
        minutes, so a publish or a correction reaches the card within that time. A `200` carries
        `Cache-Control: public, max-age=300`; an error carries none. The key check is cached for five
        minutes too: a revoked key can keep reaching the cards for up to five minutes.

        **A missing project and a failure are different answers.** When the project is unknown or has
        no audit, or `chain` is a chain Trustblock does not record, this route answers `200` with
        `isFound: false` and the "No audit on Trustblock" card - that is a real answer and is meant to be
        rendered (or hidden, on `status: false`). When the lookup itself fails, it answers an error status
        instead: `503` if a dependency was briefly unreachable, `500` otherwise. Neither is a statement
        about the project, and neither should be cached as one.
      security:
        - BearerAuth: []
      parameters:
        - { in: query, name: domain, required: false, schema: { type: string } }
        - { in: query, name: slug, required: false, schema: { type: string } }
        - in: query
          name: chain
          required: false
          description: >-
            One of the chain names below, or a decimal EVM chain id (`1`, `56`, `8453`...). A chain Trustblock does
            not record - an unknown id or name - answers `200` with `isFound: false` (no card), never an error.
          schema: { type: string, example: ethereum }
        - { in: query, name: address, required: false, schema: { type: string } }
      responses:
        '200':
          description: A card, found or not found.
          headers:
            Cache-Control:
              description: >-
                `public, max-age=300`. A card may be reused for five minutes. Error answers carry no Cache-Control.
              schema: { type: string, example: 'public, max-age=300' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GetEtherscanCardResponse' }
        '400':
          description: The query is unusable - for example `chain` without `address`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          description: No `Authorization` header was sent.
        '403':
          description: The bearer token is not a recognised key.
        '500':
          description: The lookup failed. This is not a statement about the project.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503':
          description: >-
            A dependency was briefly unreachable and the request is worth retrying. Carries
            `Retry-After`, in seconds.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /v1/project/blockscout-card:
    get:
      tags: [Cards]
      operationId: getProjectBlockscoutCard
      summary: Get a project's Blockscout card
      description: |
        The same summary as the Etherscan card, in the shape Blockscout consumes: counts and auditor
        names rather than display strings.

        Identified the same way as `/v1/project/security`. **This route consumes no API credit.**

        The counts carry the same five states as the Etherscan card, without wording: `activeRektsNumber`
        (incident), `activeCriticalIssuesNumber` (open critical), `acknowledgedCriticalIssuesNumber`
        (acknowledged critical), all three zero (none open), and `isFound: false` (no audit). It carries
        the same `contractLine`, under the same rule, the same five-minute cache on `chain` +
        `address` lookups, and the same `Cache-Control` and key-check cache.

        It shares the Etherscan card's answers: `200` with `isFound: false` when the project is unknown,
        has no audit, or `chain` is a chain Trustblock does not record; and an error status - `503` if a
        dependency was briefly unreachable, `500` otherwise - when the lookup itself fails.
      security:
        - BearerAuth: []
      parameters:
        - { in: query, name: domain, required: false, schema: { type: string } }
        - { in: query, name: slug, required: false, schema: { type: string } }
        - in: query
          name: chain
          required: false
          description: >-
            One of the chain names below, or a decimal EVM chain id (`1`, `56`, `8453`...). A chain Trustblock does
            not record - an unknown id or name - answers `200` with `isFound: false` (no card), never an error.
          schema: { type: string, example: ethereum }
        - { in: query, name: address, required: false, schema: { type: string } }
      responses:
        '200':
          description: A card, found or not found.
          headers:
            Cache-Control:
              description: >-
                `public, max-age=300`. A card may be reused for five minutes. Error answers carry no Cache-Control.
              schema: { type: string, example: 'public, max-age=300' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GetBlockscoutCardResponse' }
        '400':
          description: The query is unusable - for example `chain` without `address`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          description: No `Authorization` header was sent.
        '403':
          description: The bearer token is not a recognised key.
        '500':
          description: The lookup failed. This is not a statement about the project.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503':
          description: >-
            A dependency was briefly unreachable and the request is worth retrying. Carries
            `Retry-After`, in seconds.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /v1/auditor/upload-report-authorization:
    get:
      tags: [Publishing]
      operationId: getUploadReportAuthorization
      summary: Get an authorization to upload a report file
      description: |
        Returns a short-lived signed URL for uploading a report file, to be used before publishing an
        audit whose `reportType` is `file`.

        **Restricted to audit providers.** An integrator's API key is refused: the authorizer accepts
        a provider's session token or a provider's API key, and nothing else.

        **The response contains a signed URL and nothing else.** It does not return an `apiKey`, a
        `pinataMetadata` object or a `pinataOptions` object. Versions of this document published
        between 2025-05-02 and 2026-09-20 listed those three fields as required; they were removed from
        the API when this route moved to presigned URLs, and the document was not corrected. If you
        built against that shape, the three fields were never present and the correction is here.

        Because a call to this route mints an upload authorization, its response schema below was
        verified by reading the handler rather than by calling it.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: An upload authorization.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GetUploadReportAuthorizationResponse' }
        '401':
          description: No `Authorization` header was sent.
        '403':
          description: The caller is not an active audit provider.
  /v1/audit:
    post:
      tags: [Publishing]
      operationId: createAudit
      summary: Publish an audit
      description: |
        Publishes an audit under the calling provider's name.

        **The audit is attributed to the authenticated caller.** There is no field in the request body
        that names the provider, and none is accepted.

        **Restricted to audit providers**, as above.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateAuditRequest' }
      responses:
        '201':
          description: The audit was created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateAuditResponse' }
        '400':
          description: The body failed validation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          description: No `Authorization` header was sent.
        '403':
          description: The caller is not an active audit provider.
        '409':
          description: The audit already exists.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '413':
          description: The body is too large.
  /v1/audit/{id}:
    patch:
      tags: [Publishing]
      operationId: editAudit
      summary: Correct an audit you published
      description: |
        Replaces the name, description, conduct date, issues and contracts of an audit.

        **Only the provider that published the audit may edit it**; any other caller is refused, and
        the check is on the stored audit's provider rather than on anything in the request.

        Note that `issues` and `contracts` replace what is stored rather than merging into it.
      security:
        - BearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
          description: The audit's identifier.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EditAuditRequest' }
      responses:
        '200':
          description: The audit was updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Empty' }
        '400':
          description: The body failed validation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '401':
          description: No `Authorization` header was sent.
        '403':
          description: The caller is not an active audit provider, or the audit belongs to another provider.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404':
          description: No audit has that identifier.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer <key>`.

        For the project and card routes, the key is an API key belonging to an onboarded integrator or
        to an active audit provider. The publishing routes accept only an audit provider's credential.
        Sending no `Authorization` header produces `401`; sending one that is not recognised produces
        `403`.
  schemas:
    Extra:
      type: object
      description: Present on every response, including errors.
      required: [requestId]
      properties:
        requestId:
          type: string
          description: Quote this when reporting a problem.
    Empty:
      type: object
      required: [extra]
      properties:
        extra: { $ref: '#/components/schemas/Extra' }
    Error:
      type: object
      required: [message, extra]
      properties:
        message:
          type: string
          description: A short, non-localised description of the failure.
          example: Not found.
        code:
          type: string
          description: A stable machine-readable code, where one applies.
        diagnoses:
          type: array
          description: Field-level detail, present on validation failures.
          items: { type: object }
        extra: { $ref: '#/components/schemas/Extra' }
    Chain:
      type: string
      description: The chains Trustblock records contracts on.
      enum:
        - ethereum
        - polygon
        - avalanche
        - bnbchain
        - arbitrum
        - optimism
        - fantom
        - zksync
        - base
        - polygonZkEvm
        - scroll
        - blast
        - starknet
        - solana
        - stellar
        - sui
        - tron
        - injective
        - ton
        - plume
        - peaq
        - lukso
        - mina
        - astar
        - vana
        - fiveire
        - zircuit
        - cosmoshub
        - osmosis
        - neutron
        - terra
        - sei
        - juno
        - stargaze
        - dymension
        - archway
        - noble
        - stride
        - persistence
        - xrpl
        - sonic
        - linea
        - monad
        - mantle
        - megaeth
        - gnosis
        - plasma
        - berachain
        - celo
        - hyperevm
        - unichain
        - seievm
        - robinhood
        - arc
        - taiko
        - fraxtal
        - abstract
        - worldchain
        - apechain
        - xdc
        - katana
        - stable
        - opbnb
        - bttc
        - memecore
    Tag:
      type: string
      enum:
        - analytics
        - collectibles
        - finance
        - gaming
        - security
        - social
        - wallet
        - infrastructure
        - privacy
        - identity
        - other
    IssueStatus:
      type: string
      enum: [fixed, not_fixed]
      description: >-
        The status Trustblock publishes for a finding. A finding the client acknowledged - read, and
        accepted the risk of, without fixing it - is `not_fixed`, with `isAcknowledged: true` beside it.
    SubmittedIssueStatus:
      type: string
      enum: [fixed, not_fixed, acknowledged]
      description: >-
        The status a provider may submit. `acknowledged` means the client read the finding and accepted
        the risk; it is published as `not_fixed` with `isAcknowledged: true`.
    IssueSeverity:
      type: string
      enum: [low, medium, high, critical]
    Issue:
      type: object
      description: An issue as classified by the provider that reported it. Trustblock does not reclassify.
      required: [name, description, status, severity]
      properties:
        name: { type: string }
        description: { type: string }
        status: { $ref: '#/components/schemas/IssueStatus' }
        severity: { $ref: '#/components/schemas/IssueSeverity' }
    PublishedIssue:
      allOf:
        - $ref: '#/components/schemas/Issue'
        - type: object
          required: [id, isAcknowledged]
          properties:
            id: { type: string }
            isAcknowledged:
              type: boolean
              description: >-
                True when the provider recorded that the client acknowledged the finding - read it
                and accepted the risk - rather than fixing it. `status` is then `not_fixed`. Recorded
                for findings ingested from 2026-09 onward; earlier findings carry `false` because the
                acknowledgement was not kept, not because none existed.


                **Unmeasured** (added 2026-09-26): no live response has carried this field yet.
    InformationalIssue:
      type: object
      description: >-
        A finding its provider ranks below Low (for example "Warning" or "Info"). It is listed apart from
        `activeIssues`, has no `severity`, and is not counted anywhere: not in `activeIssues`, not in any
        count, and not in the card routes.
      required: [id, name, description, status, isAcknowledged, vendorSeverity]
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string }
        status: { $ref: '#/components/schemas/IssueStatus' }
        isAcknowledged:
          type: boolean
          description: As on `PublishedIssue`.
        vendorSeverity:
          type: string
          description: >-
            The provider's own word for the level, verbatim ("Warning", "Info"); "Informational" when the
            word was not kept.
    SubmittedIssue:
      type: object
      description: An issue as a provider submits it.
      required: [status, severity]
      properties:
        name: { type: string }
        description: { type: string }
        status: { $ref: '#/components/schemas/SubmittedIssueStatus' }
        severity: { $ref: '#/components/schemas/IssueSeverity' }
    EditedIssue:
      allOf:
        - $ref: '#/components/schemas/SubmittedIssue'
        - type: object
          properties:
            id:
              type: string
              description: An existing issue's identifier. Omitted for an issue added by this edit.
    Links:
      type: object
      required: [website]
      properties:
        website: { type: string }
        twitter: { type: string, nullable: true }
        discord: { type: string, nullable: true }
        telegram: { type: string, nullable: true }
        github: { type: string, nullable: true }
        youtube: { type: string, nullable: true }
        linkedIn: { type: string, nullable: true }
    OnChainContract:
      title: On-chain contract
      type: object
      required: [type, chain, address]
      properties:
        type: { type: string, enum: [onChain] }
        chain: { $ref: '#/components/schemas/Chain' }
        address: { type: string }
    OffChainPublicContract:
      title: Public repository contract
      type: object
      required: [type, repositoryUrl, repositoryCommitHash, repositoryFilePath]
      properties:
        type: { type: string, enum: [offChainPublic] }
        repositoryUrl: { type: string }
        repositoryCommitHash: { type: string }
        repositoryFilePath: { type: string }
    OffChainPrivateContract:
      title: Private repository contract
      type: object
      required: [type, name]
      properties:
        type: { type: string, enum: [offChainPrivate] }
        name: { type: string }
    Contract:
      oneOf:
        - $ref: '#/components/schemas/OnChainContract'
        - $ref: '#/components/schemas/OffChainPublicContract'
        - $ref: '#/components/schemas/OffChainPrivateContract'
    ProjectRekt:
      type: object
      description: A recorded incident of the project.
      required: [name, rektAt]
      properties:
        name: { type: string }
        description: { type: string }
        rektAt:
          type: number
          description: Milliseconds since the Unix epoch.
    SecurityAudit:
      type: object
      required: [date, url, auditor, isAIScan]
      properties:
        date:
          type: number
          description: >-
            Milliseconds since the Unix epoch, of when the audit was conducted. Not seconds: a value
            here is around 1.7e12, not 1.7e9.
        url:
          type: string
          description: The audit's page on Trustblock.
        isAIScan:
          type: boolean
          description: True when the audit is an automated scan rather than a human review.
        auditor:
          type: object
          required: [name, url]
          properties:
            name: { type: string }
            url: { type: string }
    GetProjectSecurityResponse:
      type: object
      required:
        [name, domain, url, audits, activeIssues, activeRekts, isDeemedDangerous, isAIScan, extra]
      properties:
        name: { type: string }
        domain: { type: string }
        url:
          type: string
          description: The project's page on Trustblock.
        audits:
          type: array
          description: Every published audit of the project, most recently conducted first.
          items: { $ref: '#/components/schemas/SecurityAudit' }
        activeIssues:
          type: array
          description: >-
            Every issue from every audit whose findings still stand, **resolved ones included** -
            read `status` on each item. The name is a misnomer and predates this document; it has
            never filtered by status.


            An earlier audit's findings are dropped only when the same provider audited the same
            contracts again: a re-audit supersedes its own earlier report. A different provider's
            findings, and findings on contracts a later audit did not cover, remain listed.


            This previously covered the most recent audit only, which meant a finding disappeared as
            soon as any newer audit was published, whoever wrote it and whatever it covered.
          items: { $ref: '#/components/schemas/PublishedIssue' }
        informationalIssues:
          type: array
          description: >-
            Optional, added 2026-10. The informational findings of the same audits - those their provider
            ranks below Low - each with the provider's own word in `vendorSeverity`. They are never in
            `activeIssues`, and `IssueSeverity` has no value for them. **Absent** when there are none, so a
            response without any is unchanged.
          items: { $ref: '#/components/schemas/InformationalIssue' }
        activeRekts:
          type: array
          items: { $ref: '#/components/schemas/ProjectRekt' }
        isDeemedDangerous:
          type: boolean
          description: |
            True when any audit whose findings still stand reports an unresolved **critical** issue, or
            the project has an unresolved recorded incident. It is a restatement of the data below it,
            not a judgement about the project.


            It tests Critical only. An unresolved **High** finding does not set it, and
            `activeHighIssuesNumber` on the card routes is published so a caller can apply its own
            threshold. Whether High should set this flag is an open product question, not an oversight:
            the great majority of unresolved High findings on this platform are advisories the client
            read and accepted.


            **It excludes acknowledged findings.** A Critical finding with `isAcknowledged: true` does
            not set it: the client read the finding and accepted the risk, and this flag describes
            unresolved findings nobody has accepted. Such a finding is still listed in `activeIssues`,
            as `not_fixed`. Acknowledgement is recorded for findings ingested from 2026-09 onward.
        isAIScan:
          type: boolean
          description: True when the most recent audit is an automated scan rather than a human review.
        extra: { $ref: '#/components/schemas/Extra' }
    EtherscanCardStatusColor:
      type: string
      description: Hex colour for the card, matching its status.
      enum: ['#F08D3C', '#E84353', '#67A22F']
    GetEtherscanCardResponse:
      type: object
      required: [isFound, imageUrl, centerText, statusColor, extra]
      properties:
        isFound:
          type: boolean
          description: >-
            False for the "No audit on Trustblock" card: an unknown project, a project with no audit, or a
            chain Trustblock does not record. `imageUrl`, `centerText` and `statusColor` are still sent; the
            other card fields are then absent.
        status:
          type: boolean
          description: The same value as `isFound`, under the name Etherscan's multichain template reads.
        contractLine:
          type: string
          description: >-
            Present only when the request named a contract (`chain` + `address`) and an audit's own scope
            names that address: "This address was in the scope of the Hacken audit of 9 Sep 2026", or
            "This address was in the scope of 2 audits; latest: Hacken, 8 May 2026". Absent otherwise,
            never null.
        imageUrl: { type: string }
        centerText:
          type: string
          description: >-
            One short line. The worst state wins: "Incident 3 Aug 2026" (red), "2 open critical findings"
            (red), "2 criticals acknowledged, not fixed" (orange), "No open critical findings"
            (green), or "No audit on Trustblock" (orange, `isFound: false`).
        statusColor: { $ref: '#/components/schemas/EtherscanCardStatusColor' }
        topLeftText: { type: string }
        topRightText:
          type: string
          description: 'For example "Last audit Aug 2026".'
        centerText2:
          type: string
          description: 'For example "3 audits by 2 firms". Plain text.'
        centerText3: { type: string }
        iconUrl: { type: string }
        url:
          type: string
          description: The project's page on Trustblock.
        isAIScan: { type: boolean }
        extra: { $ref: '#/components/schemas/Extra' }
    GetBlockscoutCardResponse:
      type: object
      required: [isFound, extra]
      properties:
        isFound:
          type: boolean
          description: >-
            False when there is no card: an unknown project, a project with no audit, or a chain Trustblock
            does not record. The response is then `isFound` and `extra` only.
        contractLine:
          type: string
          description: The same line, under the same rule, as the Etherscan card's `contractLine`. Absent otherwise.
        auditsNumber:
          type: number
          description: The project's audits on Trustblock.
        auditors:
          type: array
          description: The names of the firms that audited the project.
          items: { type: string }
        activeCriticalIssuesNumber:
          type: number
          description: >-
            Open Critical findings - neither fixed nor acknowledged - across the audits still in force.
        activeHighIssuesNumber:
          type: number
          description: >-
            Open High findings, reported beside the Critical count rather than merged into it.
            `activeCriticalIssuesNumber` counts Critical only.
        acknowledgedCriticalIssuesNumber:
          type: number
          description: >-
            Critical findings the client acknowledged without fixing, in the audits still in force.
            Not included in `activeCriticalIssuesNumber`.
        activeRektsNumber:
          type: number
          description: >-
            Unresolved rekts recorded against the project. Reported here as well as on the Etherscan
            card: a project can have no unresolved critical issue and still have been rekt.
        latestRektAt:
          type: number
          nullable: true
          description: Milliseconds since the Unix epoch, for the most recent unresolved rekt. Null when there is none.
        isAIScan: { type: boolean }
        extra: { $ref: '#/components/schemas/Extra' }
    GetUploadReportAuthorizationResponse:
      type: object
      required: [url, extra]
      properties:
        url:
          type: string
          description: A short-lived signed URL to upload the report file to.
        extra: { $ref: '#/components/schemas/Extra' }
    AuditProject:
      type: object
      required: [name, links, tags, chains]
      properties:
        name: { type: string }
        email: { type: string }
        links: { $ref: '#/components/schemas/Links' }
        tags:
          type: array
          items: { $ref: '#/components/schemas/Tag' }
        chains:
          type: array
          items: { $ref: '#/components/schemas/Chain' }
    CreateAuditRequest:
      type: object
      required: [name, description, conductedAt, reportType, project, contracts, issues]
      properties:
        name: { type: string }
        description: { type: string }
        conductedAt:
          type: number
          description: >-
            Milliseconds since the Unix epoch, of when the audit was conducted. Rejected if earlier
            than 2008-01-01 or later than the moment the request is handled.
        reportType:
          type: string
          enum: [file, web]
          description: |
            `file` requires `reportFileCid`, obtained by uploading through the authorization route.
            `web` requires `reportUrl`, a link to the provider's own published report.
        reportFileCid:
          type: string
          description: Required when `reportType` is `file`.
        reportUrl:
          type: string
          description: Required when `reportType` is `web`.
        aggregationId:
          type: string
          description: |
            Required for providers whose publishing mode is automatic or aggregated, and rejected for
            providers publishing manually.
        project: { $ref: '#/components/schemas/AuditProject' }
        contracts:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/Contract' }
        issues:
          type: array
          items: { $ref: '#/components/schemas/SubmittedIssue' }
    CreateAuditResponse:
      type: object
      required: [id, extra]
      properties:
        id:
          type: string
          description: The new audit's identifier.
        extra: { $ref: '#/components/schemas/Extra' }
    EditAuditRequest:
      type: object
      required: [name, description, conductedAt, issues, contracts]
      properties:
        name: { type: string }
        description: { type: string }
        conductedAt:
          type: number
          description: >-
            Milliseconds since the Unix epoch. Rejected if earlier than 2008-01-01 or later than the
            moment the request is handled.
        issues:
          type: array
          description: Replaces the stored issues.
          items: { $ref: '#/components/schemas/EditedIssue' }
        contracts:
          type: array
          minItems: 1
          description: Replaces the stored contracts.
          items: { $ref: '#/components/schemas/Contract' }
