# AsyncAPI 3.0 specification for the CryptoListing.ws WebSocket feed.
#
# Machine-readable companion to https://cryptolisting.ws/docs/book/.
# Hand-maintained: when a message shape changes, update this file, the .md
# chapter that documents it, and web/docs/changelog.md in the same commit.
#
# Perspective: this document describes the SERVER application. `action: send`
# therefore means "the server sends to subscribers", and `action: receive`
# means "the server accepts from a client". Flip them when generating a client.
#
# Validate with:  npx -y @asyncapi/cli validate docs/asyncapi.yaml

asyncapi: 3.0.0

info:
  title: CryptoListing.ws — Crypto Listing Announcement Feed
  version: 1.0.0
  description: |
    Real-time WebSocket feed of listing, delisting, and risk announcements published by
    Binance, Upbit, and Bithumb. Connect over WSS with an `X-API-Key` header and receive
    structured JSON the moment an announcement is detected.

    **Forward compatibility.** New fields may be added to any message at any time without
    notice. Every message schema below sets `additionalProperties: true` deliberately:
    your parser MUST ignore unknown fields and MUST NOT use strict schema validation.
    New `listingType` values may also appear — treat unknown values as inert rather than
    failing the message.

    Human documentation: https://cryptolisting.ws/docs/book/
    Single-file LLM documentation: https://cryptolisting.ws/docs/docs-llm.txt
  contact:
    name: CryptoListing.ws — @CLWfeed on Telegram
    url: https://telegram.me/CLWfeed
  externalDocs:
    description: Full documentation
    url: https://cryptolisting.ws/docs/book/

defaultContentType: application/json

servers:
  tokyo:
    host: cryptolisting.ws
    protocol: wss
    title: Tokyo
    summary: AWS Tokyo — Binance + Upbit + Bithumb
    description: |
      AWS Tokyo (ap-northeast-1a, apne1-az4). Carries all three publishers.
      Use it for bots in Tokyo or anywhere outside Korea.
    security:
      - $ref: '#/components/securitySchemes/apiKey'
  seoul:
    host: kr.cryptolisting.ws
    protocol: wss
    title: Seoul
    summary: AWS Seoul — Upbit only
    description: |
      AWS Seoul (ap-northeast-2c, apne2-az3). Carries Upbit only — Bithumb and Binance
      are dispatched from the Tokyo endpoint. Use it for Korea-based bots trading Upbit.
    security:
      - $ref: '#/components/securitySchemes/apiKey'

channels:
  feed:
    address: /
    title: Announcement feed
    description: |
      The single channel of the service. One WebSocket connection carries every message
      type. Frames are binary; payloads are UTF-8 JSON.

      Pick ONE server per bot — the closest to your trading region. The same API key
      authenticates on both, but connection caps are tracked independently per endpoint.
    servers:
      - $ref: '#/servers/tokyo'
      - $ref: '#/servers/seoul'
    bindings:
      ws:
        method: GET
        query:
          type: object
          additionalProperties: false
          properties:
            cex:
              type: string
              description: |
                Comma-separated exchange filter. Omit to receive every exchange the key
                allows. The effective scope is the INTERSECTION of the key's allow-list
                and this parameter; an empty intersection keeps the connection alive but
                delivers no announcements.
              examples:
                - binance
                - binance,upbit
                - upbit
        headers:
          type: object
          properties:
            X-API-Key:
              type: string
              pattern: '^dsk_[0-9a-f]{64}$'
              description: |
                API key. Required. Query-parameter authentication (`?api_key=`) is not
                supported and returns HTTP 401.
          required:
            - X-API-Key
        bindingVersion: 0.1.0
    messages:
      welcome:
        $ref: '#/components/messages/welcome'
      heartbeat:
        $ref: '#/components/messages/heartbeat'
      announcement:
        $ref: '#/components/messages/announcement'
      testAnnouncement:
        $ref: '#/components/messages/testAnnouncement'
      error:
        $ref: '#/components/messages/error'
      test:
        $ref: '#/components/messages/test'

operations:
  sendFeed:
    action: send
    channel:
      $ref: '#/channels/feed'
    title: Server-to-client stream
    summary: Everything the server pushes to a subscriber.
    description: |
      Order of events on a healthy connection:

      1. `welcome` — once, immediately after a successful handshake.
      2. WebSocket PING control frame every 15 s (0–5 s jitter on the first). Your library
         answers PONG automatically. No PONG within 30 s of the last ping and the server
         closes the connection.
      3. `heartbeat` — a JSON message every 30 s, every tier.
      4. `announcement` — whenever a matching event is detected. Sparse by nature: do not
         add an application-level "no message for N seconds → reconnect" watchdog.
      5. `test_announcement` — only in reply to your own `test` request.
      6. `error` — on a rejected `test` request.
    messages:
      - $ref: '#/channels/feed/messages/welcome'
      - $ref: '#/channels/feed/messages/heartbeat'
      - $ref: '#/channels/feed/messages/announcement'
      - $ref: '#/channels/feed/messages/testAnnouncement'
      - $ref: '#/channels/feed/messages/error'

  receiveTest:
    action: receive
    channel:
      $ref: '#/channels/feed'
    title: Client-to-server test request
    summary: The only message a client may send.
    description: |
      Send `{"type":"test"}` to get a synthetic `test_announcement` back — useful to
      validate a parser without waiting for a real listing. Limits: 1 test/minute (exceeding
      it returns an `error` with code `test_rate_limited`), 3 messages/minute overall
      (exceeding it closes the connection with `rate_limit_exceeded`), 1 KB max frame
      (exceeding it closes the connection with `frame_too_large`).
    messages:
      - $ref: '#/channels/feed/messages/test'

components:
  securitySchemes:
    apiKey:
      type: httpApiKey
      name: X-API-Key
      in: header
      description: |
        API key sent as an HTTP header on the WebSocket upgrade request.
        Format: `dsk_` + 64 hexadecimal characters (32 random bytes).

        Handshake failures:
          * `401` — missing `X-API-Key`
          * `403` — invalid, revoked, or expired key
          * `426` — malformed WebSocket upgrade
          * `429` — rate, per-IP, distinct-IP, or absolute-cap limit hit (or cooldown active)

        Request a key on Telegram: https://telegram.me/CLWfeed — free `SpeedTrial` and
        `FreeDelayed` keys are listed at https://cryptolisting.ws/pricing/

  messages:
    welcome:
      name: welcome
      title: Welcome
      summary: Sent once after a successful handshake; confirms tier and limits.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/Welcome'

    heartbeat:
      name: heartbeat
      title: Heartbeat
      summary: Liveness ping in JSON, every 30 s, every tier.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/Heartbeat'

    announcement:
      name: announcement
      title: Announcement
      summary: A listing, delisting, airdrop, or risk event detected on a subscribed exchange.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/Announcement'
      examples:
        - name: binanceSpotListing
          summary: Binance spot listing, single ticker
          payload:
            type: announcement
            title: Binance Will List TOKEN (TOKEN)
            ticker: TOKEN
            publisher: binance
            listingType: spot_listing
            detectedTimestampUs: 1710345000005000
            dispatchTimestampUs: 1710345000006000
            abnormalDetectionLatency: false
        - name: binanceMultiTicker
          summary: One announcement covering several tickers — `ticker` is comma-separated
          payload:
            type: announcement
            title: Binance Will List ABC, DEF and GHI
            ticker: ABC,DEF,GHI
            publisher: binance
            listingType: spot_listing
            detectedTimestampUs: 1710345000005000
            dispatchTimestampUs: 1710345000006000
            abnormalDetectionLatency: false
        - name: upbitKrwListing
          summary: Upbit KRW listing — Korean title, ticker pre-extracted
          payload:
            type: announcement
            title: 플루언트(BLEND) 신규 거래지원 안내 (KRW, BTC, USDT 마켓)
            ticker: BLEND
            publisher: upbit
            listingType: spot_listing
            detectedTimestampUs: 1710345000005000
            dispatchTimestampUs: 1710345000006000
            abnormalDetectionLatency: false
        - name: upbitCautionReleased
          summary: Upbit caution designation lifted
          payload:
            type: announcement
            title: 폴카닷(DOT) 거래 유의 종목 지정 해제 안내
            ticker: DOT
            publisher: upbit
            listingType: caution_released
            detectedTimestampUs: 1710345000005000
            dispatchTimestampUs: 1710345000006000
            abnormalDetectionLatency: false
        - name: speedTrialRedacted
          summary: Same event as seen on the SpeedTrial tier — ticker and title redacted
          payload:
            type: announcement
            title: Upgrade to see the title and ticker of this announcement
            ticker: ''
            publisher: upbit
            listingType: spot_listing
            detectedTimestampUs: 1710345000005000
            dispatchTimestampUs: 1710345000006000
            abnormalDetectionLatency: false

    testAnnouncement:
      name: test_announcement
      title: Test announcement
      summary: Synthetic announcement returned to you alone, in reply to a `test` request.
      description: |
        Identical in shape to `announcement`, with `type` = `test_announcement` and
        `ticker` = `DUMMYTOKEN`. Always carries the full payload regardless of tier —
        use it to validate your parser on any tier, including SpeedTrial.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/TestAnnouncement'

    error:
      name: error
      title: Error
      summary: Sent when a client request is rejected without closing the connection.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/Error'
      examples:
        - name: testRateLimited
          payload:
            type: error
            code: test_rate_limited
            retryAfterSecs: 42

    test:
      name: test
      title: Test request
      summary: The only client-to-server message.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/TestRequest'
      examples:
        - name: test
          payload:
            type: test

  schemas:
    Welcome:
      type: object
      # Unknown fields may be added at any time — ignore them, never validate strictly.
      additionalProperties: true
      required:
        - type
        - tier
        - allowedCex
      properties:
        type:
          type: string
          const: welcome
        tier:
          type: string
          enum: [SpeedTrial, FreeDelayed, basic, premium]
          description: See the tier table in the Message Reference chapter.
        maxDistinctIps:
          type: integer
          description: Max distinct IPs that can hold connections with this key.
        maxConnectionsPerIp:
          type: integer
          description: Per-IP cap — fixed at 3.
        absoluteMaxConnections:
          type: integer
          description: Hard cap across all IPs — fixed at 20.
        allowedCex:
          type: string
          description: Effective filter after key restriction × `?cex=`. `*` means all.
          examples:
            - '*'
            - binance,upbit
        expiresInSecs:
          type:
            - integer
            - 'null'
          description: Seconds until the key expires; `null` = no expiration.

    Heartbeat:
      type: object
      additionalProperties: true
      required:
        - type
      properties:
        type:
          type: string
          const: heartbeat
        timestampNs:
          type: integer
          description: Server time, nanoseconds since Unix epoch.
          examples:
            - 1710345030123456789
        timeUtc:
          type: string
          format: date-time
          description: Server time, ISO 8601 UTC.
          examples:
            - '2026-04-17T08:30:30.123456Z'

    Announcement:
      type: object
      additionalProperties: true
      required:
        - type
        - title
        - ticker
        - publisher
        - listingType
        - detectedTimestampUs
        - dispatchTimestampUs
      properties:
        type:
          type: string
          const: announcement
        title:
          type: string
          description: |
            The exchange's own, unmodified announcement title — Korean on Upbit and
            Bithumb, English on Binance. Replaced by an upgrade notice on the SpeedTrial
            tier for every type except `not_listing`.
        ticker:
          type: string
          description: |
            Asset symbol(s), pre-extracted from `title`. ALWAYS parse as a potentially
            comma-separated list: `tickers = msg["ticker"].split(",")`. Empty string on
            the SpeedTrial tier for every type except `not_listing`.
          examples:
            - TOKEN
            - ABC,DEF,GHI
            - ''
        publisher:
          $ref: '#/components/schemas/Publisher'
        listingType:
          $ref: '#/components/schemas/ListingType'
        detectedTimestampUs:
          type: integer
          description: Detection time, microseconds since Unix epoch.
        dispatchTimestampUs:
          type: integer
          description: Dispatch time, microseconds since Unix epoch.
        abnormalDetectionLatency:
          type: boolean
          description: |
            `true` when the exchange publish → detection interval was abnormally high.
            The payload is still valid; treat it as a hint that the event may be stale.

    TestAnnouncement:
      type: object
      additionalProperties: true
      required:
        - type
        - title
        - ticker
        - publisher
        - listingType
      properties:
        type:
          type: string
          const: test_announcement
        title:
          type: string
        ticker:
          type: string
          const: DUMMYTOKEN
        publisher:
          $ref: '#/components/schemas/Publisher'
        listingType:
          $ref: '#/components/schemas/ListingType'
        detectedTimestampUs:
          type: integer
        dispatchTimestampUs:
          type: integer
        abnormalDetectionLatency:
          type: boolean

    Error:
      type: object
      additionalProperties: true
      required:
        - type
        - code
      properties:
        type:
          type: string
          const: error
        code:
          type: string
          description: Machine-readable error code.
          examples:
            - test_rate_limited
        retryAfterSecs:
          type: integer
          description: Seconds to wait before retrying the rejected request.

    TestRequest:
      type: object
      additionalProperties: false
      required:
        - type
      properties:
        type:
          type: string
          const: test

    Publisher:
      type: string
      enum:
        - binance
        - upbit
        - bithumb
      description: Lowercase exchange name that published the announcement.

    ListingType:
      type: string
      description: |
        Event class, pre-parsed from the announcement title. New values may be added
        without notice — treat an unrecognised value as inert instead of erroring.
      enum:
        - spot_listing
        - spot_delisting
        - futures_listing
        - futures_delisting
        - hodler_airdrop
        - monitoring_tag_extend
        - monitoring_tag_remove
        - caution_released
        - not_listing
      x-listing-type-meanings:
        spot_listing: New spot market listing — Binance, Upbit, Bithumb.
        spot_delisting: Spot market delisting — Binance, Upbit, Bithumb.
        futures_listing: New futures/perpetual listing — Binance.
        futures_delisting: Futures/perpetual delisting — Binance.
        hodler_airdrop: Binance HODLer Airdrop — Binance.
        monitoring_tag_extend: Token added to Binance's Monitoring Tag — Binance.
        monitoring_tag_remove: Token removed from Binance's Monitoring Tag — Binance.
        caution_released: Caution designation lifted — Bithumb, Upbit.
        not_listing: Other announcement (maintenance, token swap, etc.) — Binance.
