openapi: 3.1.0
info:
  title: Aradia Data API
  version: 1.0.0
  description: Read-only NFT data API for Astar mainnet.
  contact:
    url: https://docs.aradia.app
servers:
  - url: https://api.aradia.app
    description: Production
security:
  - bearerAuth: []
tags:
  # Named "Demo token" rather than "Authentication" so the API Reference group in
  # the sidebar does not repeat the label of the Authentication guide.
  - name: Demo token
  - name: Collections
  - name: NFTs
  - name: Transfers
  - name: Activity
  - name: Status
  - name: Media
paths:
  /v1/demo-token:
    post:
      operationId: issueDemoToken
      tags: [Demo token]
      summary: Issue a short-lived read-only demo token
      description: >-
        Mints a throwaway credential so the documentation console can call the API
        without anyone signing up. This is the only endpoint that takes no
        credential, because it is the one that produces them.

        The token is read-only, expires 15 minutes after it is issued, and carries
        the demo ceiling of 1 request per second. Only the token's hash is stored,
        so a token cannot be recovered after this response: request another one.

        Issuance itself is limited to 5 per minute per IP address. Use a real API
        key for anything beyond trying an endpoint.
      security: []
      responses:
        '200':
          description: A new demo token. Never cached.
          headers:
            Cache-Control:
              description: Always no-store, because the body carries a credential.
              schema: { type: string }
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties: { data: { $ref: '#/components/schemas/DemoTokenGrant' } }
              examples:
                default: { $ref: '#/components/examples/DemoToken' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/collections:
    get:
      operationId: listCollections
      tags: [Collections]
      summary: List Astar NFT collections
      description: >-
        Newest first, ordered by indexing time. Collections whose metadata is
        recorded as empty are omitted, so this list is narrower than the full set
        of contracts the indexer has seen. Fetch a single collection by address
        with GET /v1/collections/{address}, which applies no such filter.
      security: [{ bearerAuth: [read:collections] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/BeforeTimestamp'
        - $ref: '#/components/parameters/BeforeID'
        - $ref: '#/components/parameters/Network'
        - name: search
          in: query
          description: >-
            Case-insensitive substring match on collection name and symbol. A full
            contract address is treated as an exact lookup on that collection.
          schema: { type: string, maxLength: 200 }
      responses:
        '200':
          description: Collection page
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CollectionListResult' }
              examples:
                default: { $ref: '#/components/examples/CollectionPage' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
    head:
      operationId: inspectCollections
      tags: [Collections]
      summary: Inspect collection-list availability without a response body
      security: [{ bearerAuth: [read:collections] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/BeforeTimestamp'
        - $ref: '#/components/parameters/BeforeID'
        - $ref: '#/components/parameters/Network'
        - name: search
          in: query
          description: >-
            Case-insensitive substring match on collection name and symbol. A full
            contract address is treated as an exact lookup on that collection.
          schema: { type: string, maxLength: 200 }
      responses:
        '200': { description: Collection endpoint is available }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/collections/{address}:
    get:
      operationId: getCollection
      tags: [Collections]
      summary: Get one collection
      security: [{ bearerAuth: [read:collections] }]
      parameters:
        - $ref: '#/components/parameters/CollectionAddress'
        - $ref: '#/components/parameters/Network'
      responses:
        '200':
          description: Collection
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties: { data: { $ref: '#/components/schemas/Collection' } }
              examples:
                default: { $ref: '#/components/examples/CollectionDetail' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/collections/{address}/nfts:
    get:
      operationId: listCollectionNFTs
      tags: [NFTs]
      summary: List NFTs in a collection
      security: [{ bearerAuth: [read:nfts] }]
      parameters:
        - $ref: '#/components/parameters/CollectionAddress'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/AfterTokenID'
        - $ref: '#/components/parameters/AfterID'
        - name: include_burned
          in: query
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: NFT page
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NFTListResult' }
              examples:
                default: { $ref: '#/components/examples/NFTPage' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/nfts/{address}/{tokenId}:
    get:
      operationId: getNFT
      tags: [NFTs]
      summary: Get one NFT
      security: [{ bearerAuth: [read:nfts] }]
      parameters:
        - $ref: '#/components/parameters/CollectionAddress'
        - $ref: '#/components/parameters/TokenId'
      responses:
        '200':
          description: NFT
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties: { data: { $ref: '#/components/schemas/NFT' } }
              examples:
                default: { $ref: '#/components/examples/NFTDetail' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/owners/{address}/nfts:
    get:
      operationId: listOwnerNFTs
      tags: [NFTs]
      summary: List NFTs currently owned by an address
      security: [{ bearerAuth: [read:owners] }]
      parameters:
        - name: address
          in: path
          required: true
          schema: { $ref: '#/components/schemas/Address' }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/AfterCollection'
        - $ref: '#/components/parameters/AfterTokenID'
        - $ref: '#/components/parameters/AfterID'
      responses:
        '200':
          description: NFT page
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NFTListResult' }
              examples:
                default: { $ref: '#/components/examples/NFTPage' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/collections/{address}/transfers:
    get:
      operationId: listCollectionTransfers
      tags: [Transfers]
      summary: List canonical-aware collection transfers
      security: [{ bearerAuth: [read:transfers] }]
      parameters:
        - $ref: '#/components/parameters/CollectionAddress'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/BeforeTimestamp'
        - $ref: '#/components/parameters/BeforeID'
      responses:
        '200':
          description: Transfer page
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TransferListResult' }
              examples:
                default: { $ref: '#/components/examples/TransferPage' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/nfts/{address}/{tokenId}/transfers:
    get:
      operationId: listNFTTransfers
      tags: [Transfers]
      summary: List canonical-aware transfers for one NFT
      security: [{ bearerAuth: [read:transfers] }]
      parameters:
        - $ref: '#/components/parameters/CollectionAddress'
        - $ref: '#/components/parameters/TokenId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/BeforeTimestamp'
        - $ref: '#/components/parameters/BeforeID'
      responses:
        '200':
          description: Transfer page
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TransferListResult' }
              examples:
                default: { $ref: '#/components/examples/TransferPage' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/activity:
    get:
      operationId: listActivity
      tags: [Activity]
      summary: List global transfer activity
      description: >-
        Newest first. Only canonical rows are returned: transfers invalidated by a
        chain reorganisation are excluded. event_type is derived consistently for
        both filtering and output, where a transfer from the zero address is a
        mint and one to the zero address is a burn.
      security: [{ bearerAuth: [read:activity] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/BeforeTimestamp'
        - $ref: '#/components/parameters/BeforeID'
        - $ref: '#/components/parameters/Network'
        - name: collection
          in: query
          schema: { $ref: '#/components/schemas/Address' }
        - name: event_type
          in: query
          schema: { type: string, enum: [mint, transfer, burn] }
      responses:
        '200':
          description: Activity page
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ActivityListResult' }
              examples:
                default: { $ref: '#/components/examples/ActivityPage' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/indexer/status:
    get:
      operationId: getIndexerStatus
      tags: [Status]
      summary: Get sanitized indexer coverage status
      security: [{ bearerAuth: [read:status] }]
      parameters: [{ $ref: '#/components/parameters/Network' }]
      responses:
        '200':
          description: Indexer status
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties: { data: { $ref: '#/components/schemas/IndexerStatus' } }
              examples:
                default: { $ref: '#/components/examples/IndexerStatus' }
        '400': { $ref: '#/components/responses/InvalidArgument' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /v1/media:
    get:
      operationId: getStoredMedia
      tags: [Media]
      summary: Serve a locally stored NFT original
      description: >-
        Returns the original bytes for a source URL only when Aradia has already
        stored that asset. This route never fetches on demand. Use the media_url
        returned on an NFT instead of constructing this request yourself; when
        media_url is absent, fall back to image_url.
      security: []
      x-reference: false
      parameters:
        - name: url
          in: query
          required: true
          description: The exact source URL recorded for the NFT.
          schema: { type: string, minLength: 1, maxLength: 2000 }
      responses:
        '200':
          description: Stored image, animation, audio, or video bytes
          headers:
            Cache-Control:
              description: Immutable one-year browser cache for content-addressed bytes.
              schema: { type: string }
            X-Media-Cache:
              description: Whether the original was served from the local cache.
              schema: { type: string, enum: [original, hit] }
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
              example: stored media bytes
        '400':
          description: Missing or invalid source URL
          content: { text/plain: { schema: { type: string }, example: url is required } }
        '404':
          description: The source is unknown, unavailable, or not stored locally
          content: { text/plain: { schema: { type: string }, example: not found } }
        '502':
          description: The media catalogue could not be read
          content: { text/plain: { schema: { type: string }, example: lookup failed } }
    head:
      operationId: inspectStoredMedia
      tags: [Media]
      summary: Inspect locally stored media without a response body
      security: []
      x-reference: false
      parameters:
        - name: url
          in: query
          required: true
          schema: { type: string, minLength: 1, maxLength: 2000 }
      responses:
        '200': { description: The stored asset is available }
        '400': { description: Missing or invalid source URL }
        '404': { description: The source is not stored locally }
        '502': { description: The media catalogue could not be read }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Aradia API key or demo token
      description: |
        Send your key in the `Authorization` header as `Bearer <key>`.

        A key is `ara_` followed by 33 lowercase alphanumeric characters, so it
        looks like `ara_w32juoe6` plus 25 more. The first 8 characters after the
        prefix are the public identifier shown in the developer portal; the
        remaining 25 are the secret and are displayed only once, when the key is
        created.

        No complete key appears in this documentation on purpose: a full-length
        example is indistinguishable from a leaked credential, both to a reader
        and to a secret scanner.

        Credentials are rejected if they appear in the query string.
  parameters:
    Limit:
      name: limit
      in: query
      description: How many items to return per page. Defaults to 20, maximum 100.
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    BeforeTimestamp:
      name: before_timestamp
      in: query
      description: >-
        Page boundary: return rows older than this instant, expressed as a Unix
        timestamp in microseconds. Copy the value from next_page_params. Must be
        sent together with before_id.
      schema: { type: integer, format: int64, minimum: 1 }
    BeforeID:
      name: before_id
      in: query
      description: >-
        Page boundary tie-break: the row id from next_page_params. Timestamps
        repeat heavily, so the id is required to place the boundary exactly. Must
        be sent together with before_timestamp.
      schema: { type: integer, format: int64, minimum: 1 }
    AfterTokenID:
      name: after_token_id
      in: query
      description: >-
        Page boundary for token listings: return tokens after this id. Copy the
        value from next_page_params. Must be sent together with after_id.
      schema: { type: string, pattern: '^[0-9]{1,78}$' }
    AfterID:
      name: after_id
      in: query
      description: >-
        Page boundary tie-break for token listings: the row id from
        next_page_params. Must be sent together with after_token_id.
      schema: { type: integer, format: int64, minimum: 1 }
    AfterCollection:
      name: after_collection
      in: query
      description: >-
        Page boundary for owner holdings, which are ordered across collections.
        Required alongside after_token_id and after_id on that endpoint only.
      schema: { $ref: '#/components/schemas/Address' }
    Network:
      name: network
      in: query
      description: Chain to query. Only Astar mainnet is indexed today.
      schema: { type: string, enum: [astar], default: astar }
    CollectionAddress:
      name: address
      in: path
      required: true
      description: Collection contract address, lowercase hex with the 0x prefix.
      schema: { $ref: '#/components/schemas/Address' }
    TokenId:
      name: tokenId
      in: path
      required: true
      description: Token id as a decimal string, so very large ids keep full precision.
      schema: { type: string, pattern: '^[0-9]{1,78}$' }
  responses:
    InvalidArgument:
      description: Invalid parameter or cursor
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
          example: { error: { code: INVALID_ARGUMENT, message: One or more request parameters are invalid. } }
    Unauthorized:
      description: Missing or invalid bearer credential
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
          example: { error: { code: UNAUTHORIZED, message: Authentication is required. } }
    Forbidden:
      description: Credential lacks the required read-only scope
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
          example: { error: { code: FORBIDDEN, message: The API key does not have the required scope. } }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
          example: { error: { code: NOT_FOUND, message: The requested resource was not found. } }
    RateLimited:
      description: Request rate limit exceeded
      headers:
        Retry-After: { schema: { type: integer } }
        RateLimit-Reset: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
          example: { error: { code: RATE_LIMITED, message: Request rate limit exceeded. } }
    Unavailable:
      description: Temporary dependency failure
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
          example: { error: { code: SERVICE_UNAVAILABLE, message: The data service is temporarily unavailable. } }
  examples:
    DemoToken:
      summary: A freshly issued demo token
      value:
        data:
          token: 'ara_demo_AAAAAAAAAAAAAAAA_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'
          expires_at: '2026-08-18T12:15:00Z'
          scopes: [read:collections, read:nfts, read:owners, read:transfers, read:activity, read:status]
          limits: { requests_per_second: 1, requests_per_minute: 30, concurrent: 2 }
    CollectionPage:
      summary: First page of tracked collections
      value:
        data:
          - contract_address: '0x0000000000000000000000000000000000000001'
            network: astar
            name: Example Collection
            symbol: EXCOL
            image_url: 'https://wsrv.nl/?url=https%3A%2F%2Fipfs.io%2Fipfs%2FQmExample%2F1.png'
            contract_type: ERC721
            total_supply: '10000'
            unique_holders: '2799'
            total_transfers: '11163'
            creator_address: '0x00000000000000000000000000000000000000c7'
            deploy_tx_hash: '0x0000000000000000000000000000000000000000000000000000000000000abc'
            created_at: '2026-08-01T00:00:00Z'
        pagination:
          limit: 20
          has_more: true
          next_page_params: { before_timestamp: 1780245600123456, before_id: 26228 }
        coverage: { status: unknown }
    CollectionDetail:
      summary: One collection with its indexed counters
      value:
        data:
          contract_address: '0x0000000000000000000000000000000000000001'
          network: astar
          name: Example Collection
          image_url: 'https://wsrv.nl/?url=https%3A%2F%2Fipfs.io%2Fipfs%2FQmExample%2F1.png'
          contract_type: ERC721
          total_supply: '10000'
          unique_holders: '2799'
          total_transfers: '11163'
          creator_address: '0x00000000000000000000000000000000000000c7'
          deploy_tx_hash: '0x0000000000000000000000000000000000000000000000000000000000000abc'
          created_at: '2026-08-01T00:00:00Z'
    NFTPage:
      summary: First page of collection tokens
      value:
        data:
          - collection_address: '0x0000000000000000000000000000000000000001'
            token_id: '1'
            network: astar
            current_owner: '0x0000000000000000000000000000000000000002'
            is_burned: false
            transfer_count: 3
            metadata_uri: 'ipfs://bafkreiexampleexampleexampleexample/1.json'
            image_url: 'ipfs://bafkreiexampleexampleexampleexample/1.png'
            media_url: 'https://api.aradia.app/v1/media?url=ipfs%3A%2F%2Fbafkreiexampleexampleexampleexample%2F1.png'
            collection_name: Example Collection
            collection_symbol: EXCOL
        pagination:
          limit: 20
          has_more: true
          next_page_params: { after_token_id: '1', after_id: 91422 }
        coverage: { status: unknown }
    NFTDetail:
      summary: One NFT with current ownership
      value:
        data:
          collection_address: '0x0000000000000000000000000000000000000001'
          token_id: '1'
          network: astar
          current_owner: '0x0000000000000000000000000000000000000002'
          is_burned: false
          transfer_count: 1
          collection_name: Example Collection
          collection_symbol: EXCOL
    TransferPage:
      summary: Transfers, newest first, with depth per row
      value:
        data:
          - collection_address: '0x0000000000000000000000000000000000000001'
            token_id: '1'
            network: astar
            from_address: '0x0000000000000000000000000000000000000000'
            to_address: '0x0000000000000000000000000000000000000002'
            event_type: mint
            block_number: '7654321'
            log_index: 4
            transaction_hash: '0x1111111111111111111111111111111111111111111111111111111111111111'
            block_timestamp: '2026-08-01T12:00:00Z'
            confirmations: 1284
          # Backfilled rows carry no block number, so their depth is unknowable
          # and reported as null rather than invented.
          - collection_address: '0x0000000000000000000000000000000000000001'
            token_id: '2'
            network: astar
            from_address: '0x0000000000000000000000000000000000000002'
            to_address: '0x0000000000000000000000000000000000000003'
            event_type: transfer
            block_number: '0'
            log_index: 0
            transaction_hash: '0x2222222222222222222222222222222222222222222222222222222222222222'
            block_timestamp: '2026-03-30T09:15:00Z'
            confirmations: null
        pagination:
          limit: 20
          has_more: true
          next_page_params: { before_timestamp: 1780245600123456, before_id: 2017853 }
        coverage: { status: partial, start_block: '7000000' }
    ActivityPage:
      summary: Global activity feed, newest first
      value:
        data:
          - collection_address: '0x0000000000000000000000000000000000000001'
            collection_name: Example Collection
            token_id: '1'
            network: astar
            from_address: '0x0000000000000000000000000000000000000000'
            to_address: '0x0000000000000000000000000000000000000002'
            event_type: mint
            block_number: '7654321'
            log_index: 4
            transaction_hash: '0x1111111111111111111111111111111111111111111111111111111111111111'
            block_timestamp: '2026-08-01T12:00:00Z'
            confirmations: 1284
        pagination:
          limit: 20
          has_more: true
          next_page_params: { before_timestamp: 1780245600123456, before_id: 2017853 }
        coverage: { status: unknown }
    IndexerStatus:
      summary: Indexer status before coverage verification
      value:
        data:
          network: astar
          coverage_status: unknown
          last_confirmed_block: '123'
          last_seen_block: '135'
          updated_at: '2026-08-01T00:00:00Z'
  schemas:
    Address:
      type: string
      pattern: '^0x[0-9a-f]{40}$'
      description: Lowercase EVM address.
    Pagination:
      type: object
      required: [limit, has_more]
      description: >-
        Keyset pagination. When has_more is true, next_page_params holds the query
        parameters that return the following page: copy each key and value into the
        next request, keeping the other filters unchanged. The keys vary by
        endpoint, because each one pages on its own ordering.
      properties:
        limit: { type: integer, minimum: 1, maximum: 100 }
        has_more: { type: boolean }
        next_page_params:
          type: object
          nullable: true
          description: >-
            Absent when has_more is false. Time-ordered feeds return
            before_timestamp and before_id; token listings return after_token_id
            and after_id, plus after_collection on the owner holdings endpoint.
          additionalProperties: true
          example:
            before_timestamp: 1780245600123456
            before_id: 2017853
    DemoTokenGrant:
      type: object
      required: [token, expires_at, scopes, limits]
      properties:
        token: { type: string, description: 'Bearer credential, prefixed ara_demo_. Send it as Authorization: Bearer. Not recoverable later.' }
        expires_at: { type: string, format: date-time, description: When the token stops authenticating. 15 minutes after issuance. }
        scopes: { type: array, items: { type: string }, description: Read-only scopes granted. Never includes a write scope. }
        limits:
          type: object
          required: [requests_per_second, requests_per_minute, concurrent]
          description: The ceiling this token is subject to, so a client can pace itself instead of discovering it through 429s.
          properties:
            requests_per_second: { type: integer }
            requests_per_minute: { type: integer }
            concurrent: { type: integer }
    Coverage:
      type: object
      required: [status]
      description: >-
        Whether the indexed window behind this response is complete. It is a
        property of the query, not of any single row, so it is reported once per
        response rather than repeated on every item.
      properties:
        status: { type: string, enum: [unknown, partial, complete] }
        start_block: { type: string, nullable: true, description: 'First block covered, when known. Decimal string.' }
    Collection:
      type: object
      required: [contract_address, network, name, contract_type, total_supply, unique_holders, total_transfers, created_at]
      properties:
        contract_address: { $ref: '#/components/schemas/Address' }
        network: { type: string, enum: [astar] }
        name: { type: string }
        symbol: { type: string, nullable: true }
        description: { type: string, nullable: true }
        image_url:
          type: string
          format: uri
          nullable: true
          description: >-
            Collection image. The contract's own collection image when it declares
            one, otherwise the image of its first NFT by ascending token_id. Stable
            across calls, because the source is the lowest token id and never an
            arbitrary one, so the value can be cached. Null when neither exists.
        contract_type: { type: string, description: Token standard, for example ERC721 or ERC1155. }
        total_supply: { type: string, description: 'Tokens observed for this contract. Decimal string, so a uint256 survives the round trip.' }
        unique_holders: { type: string, description: 'Distinct current owners. Decimal string.' }
        total_transfers: { type: string, description: 'Transfer events observed. Decimal string.' }
        creator_address: { $ref: '#/components/schemas/Address' }
        deploy_tx_hash: { type: string, nullable: true, description: Deployment transaction, so the contract can be audited against the chain. }
        created_at: { type: string, format: date-time, description: When the collection was first indexed. }
    NFT:
      type: object
      required: [collection_address, token_id, network, current_owner, is_burned, transfer_count]
      properties:
        collection_address: { $ref: '#/components/schemas/Address' }
        token_id: { type: string, pattern: '^[0-9]{1,78}$' }
        network: { type: string, enum: [astar] }
        current_owner: { $ref: '#/components/schemas/Address' }
        metadata_uri: { type: string, nullable: true }
        name: { type: string, nullable: true }
        description: { type: string, nullable: true }
        image_url:
          type: string
          format: uri
          nullable: true
          description: Original metadata image source. Use as the fallback when media_url is absent.
        media_url:
          type: string
          format: uri
          nullable: true
          description: >-
            Aradia-hosted original bytes. Present only when the source has already
            been stored locally, so clients may use it without probing for a cache
            miss. Omitted when the cache has no stored copy; then use image_url.
        attributes: { type: object, nullable: true }
        media_type: { type: string, nullable: true }
        collection_name: { type: string, nullable: true, description: 'Name of the collection this token belongs to. Null when the collection has not been indexed yet.' }
        collection_symbol: { type: string, nullable: true, description: Symbol of the collection this token belongs to. }
        is_burned: { type: boolean }
        transfer_count: { type: integer, minimum: 0 }
        minted_at: { type: string, format: date-time, nullable: true }
        last_transfer_at: { type: string, format: date-time, nullable: true }
    Transfer:
      type: object
      required: [collection_address, token_id, network, from_address, to_address, transaction_hash, block_number, block_timestamp, log_index, event_type, confirmations]
      properties:
        collection_address: { $ref: '#/components/schemas/Address' }
        token_id: { type: string }
        network: { type: string, enum: [astar] }
        from_address: { $ref: '#/components/schemas/Address' }
        to_address: { $ref: '#/components/schemas/Address' }
        transaction_hash: { type: string, pattern: '^0x[0-9a-f]{64}$' }
        block_number: { type: string }
        block_timestamp: { type: string, format: date-time }
        log_index: { type: integer, minimum: 0 }
        event_type: { type: string, enum: [mint, transfer, burn] }
        confirmations:
          type: integer
          format: int64
          minimum: 0
          nullable: true
          description: >-
            Blocks mined on top of this transfer, so you can apply your own finality
            threshold instead of inheriting ours. Aradia shows a transfer at 3 and
            treats it as final at 12.

            Null, and always present rather than omitted, when the depth is not
            knowable: the historical backfill populated timestamps without block
            numbers, so 98.7% of transfers carry block_number 0 and any count for
            them would be fiction.
    Activity:
      allOf:
        - $ref: '#/components/schemas/Transfer'
        - type: object
          properties: { collection_name: { type: string, nullable: true } }
    IndexerStatus:
      type: object
      required: [network, coverage_status, last_confirmed_block, last_seen_block, updated_at]
      properties:
        network: { type: string, enum: [astar] }
        coverage_status: { type: string, enum: [unknown, partial, complete] }
        coverage_start_block: { type: string, nullable: true }
        last_confirmed_block: { type: string }
        last_seen_block: { type: string }
        coverage_verified_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time }
    CollectionListResult:
      type: object
      required: [data, pagination, coverage]
      properties: { data: { type: array, items: { $ref: '#/components/schemas/Collection' } }, pagination: { $ref: '#/components/schemas/Pagination' }, coverage: { $ref: '#/components/schemas/Coverage' } }
    NFTListResult:
      type: object
      required: [data, pagination, coverage]
      properties: { data: { type: array, items: { $ref: '#/components/schemas/NFT' } }, pagination: { $ref: '#/components/schemas/Pagination' }, coverage: { $ref: '#/components/schemas/Coverage' } }
    TransferListResult:
      type: object
      required: [data, pagination, coverage]
      properties: { data: { type: array, items: { $ref: '#/components/schemas/Transfer' } }, pagination: { $ref: '#/components/schemas/Pagination' }, coverage: { $ref: '#/components/schemas/Coverage' } }
    ActivityListResult:
      type: object
      required: [data, pagination, coverage]
      properties: { data: { type: array, items: { $ref: '#/components/schemas/Activity' } }, pagination: { $ref: '#/components/schemas/Pagination' }, coverage: { $ref: '#/components/schemas/Coverage' } }
    ErrorBody:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, enum: [INVALID_ARGUMENT, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, RATE_LIMITED, RATE_LIMITER_UNAVAILABLE, SERVICE_UNAVAILABLE, INTERNAL_ERROR] }
            message: { type: string }
            request_id: { type: string }
