openapi: 3.0.3
info:
  title: ThunderLaunch RugCheck Public API
  version: "1.0"
  description: |
    Token-safety reports and risk scores for any Solana mint, powered by the
    same production security engine behind [RugCheck](https://thunderlaunch.fun/rugcheck).

    - **Anonymous access works** (per-IP rate limits). An API key (create one at
      [/profile/api-keys](https://thunderlaunch.fun/profile/api-keys) with the
      `rugcheck:read` permission) unlocks your key's own rate limit and monthly
      fresh-scan quota.
    - **Stability**: v1 response shapes only change additively — clients must
      ignore unknown fields. Breaking changes would ship as `/api/v2`.
    - Cached reads are free and unlimited on every tier; only fresh scans
      (uncached mints) count against monthly quotas.

    Human-readable docs: https://thunderlaunch.fun/developers
  termsOfService: https://thunderlaunch.fun/developers#terms
  contact:
    email: support@thunderlaunch.fun
servers:
  - url: https://thunderlaunch.fun
security:
  - {}
  - ApiKeyHeader: []
  - BearerAuth: []
tags:
  - name: RugCheck
    description: Token safety reports and risk scores

paths:
  /api/v1/rugcheck/{mint}:
    get:
      tags: [RugCheck]
      operationId: getRugcheckReport
      summary: Full safety report for a Solana mint
      description: >
        Complete report: scores, verdict, hard-cap flags, market and holder
        context, creator reputation, and the evidence-backed findings list.
        Served from cache when warm (~5 min TTL); built fresh on a cold miss
        (can take several seconds).
      parameters:
        - $ref: "#/components/parameters/Mint"
        - name: forceRefresh
          in: query
          required: false
          description: >
            Bypass the report cache and build fresh. Requires an API key on the
            Pro or Scale tier (`paid_tier_required` otherwise); the forced
            build counts against the monthly fresh-scan quota.
          schema:
            type: boolean
      responses:
        "200":
          description: Safety report
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: "#/components/schemas/RugReport"
        "400": { $ref: "#/components/responses/InvalidMint" }
        "401": { $ref: "#/components/responses/InvalidKey" }
        "403": { $ref: "#/components/responses/MissingScope" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ScanFailed" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /api/v1/rugcheck/batch:
    post:
      tags: [RugCheck]
      operationId: batchRugcheck
      summary: Resolve many mints in one call (API key required)
      description: >
        Keyed-only. Per-tier batch caps: Free 10 / Pro 50 / Scale 100 mints per
        call. Each mint resolves independently (cached-first; fresh builds count
        against the monthly quota) — the batch responds 200 with per-item
        ok/error entries, and one batch call counts as ONE request against the
        key's rate limit. Default items carry the lightweight risk shape;
        `full: true` returns complete reports.
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mints]
              properties:
                mints:
                  type: array
                  minItems: 1
                  items: { type: string }
                  description: Base58 mint addresses (deduped server-side)
                full:
                  type: boolean
                  default: false
                  description: Return complete RugReports instead of risk shapes
      responses:
        "200":
          description: Per-item results (batch itself succeeds even when items fail)
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    required: [total, ok, failed, results]
                    properties:
                      total: { type: integer }
                      ok: { type: integer }
                      failed: { type: integer }
                      results:
                        type: array
                        items: { $ref: "#/components/schemas/BatchItem" }
        "400":
          description: Invalid body or batch over the tier cap (`invalid_request`, `batch_too_large`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401":
          description: Missing (`key_required`) or invalid (`invalid_key`) API key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403": { $ref: "#/components/responses/MissingScope" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /api/v1/rugcheck/{mint}/history:
    get:
      tags: [RugCheck]
      operationId: getMintHistory
      summary: Scan history + monitor time series for a mint (API key required)
      description: >
        Durable summary (first scan, scan count, terminal rug state,
        peak-liquidity drawdown) plus the security monitor's time series
        (holder count, liquidity, price, net flow — 10-minute cadence, 30-day
        retention, oldest-first, max 500 points). Keyed-only, any tier; pure
        DB reads — never consumes fresh-scan quota. 404 `not_found` when the
        mint has never been scanned.
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Mint"
      responses:
        "200":
          description: History
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, enum: [true] }
                  data: { $ref: "#/components/schemas/MintHistory" }
        "400": { $ref: "#/components/responses/InvalidMint" }
        "401":
          description: Missing (`key_required`) or invalid (`invalid_key`) API key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403": { $ref: "#/components/responses/MissingScope" }
        "404":
          description: Mint never scanned (`not_found`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /api/v1/deployer/{wallet}:
    get:
      tags: [RugCheck]
      operationId: getDeployerReputation
      summary: Deployer/creator reputation for a wallet (API key required)
      description: >
        Launch count, rug outcomes, serial-rugger flag, funding-source class
        (when known from scans), and the wallet's most recent rugged tokens.
        Keyed-only, any tier; pure DB reads — never consumes fresh-scan quota.
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      parameters:
        - name: wallet
          in: path
          required: true
          description: Base58 Solana wallet address
          schema: { type: string }
      responses:
        "200":
          description: Reputation
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, enum: [true] }
                  data: { $ref: "#/components/schemas/WalletReputation" }
        "400":
          description: Not a valid base58 wallet (`invalid_wallet`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401":
          description: Missing (`key_required`) or invalid (`invalid_key`) API key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403": { $ref: "#/components/responses/MissingScope" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /api/v1/webhooks:
    post:
      tags: [RugCheck]
      operationId: createWebhook
      summary: Subscribe a URL to rugged alerts (Scale tier)
      description: >
        Requires an API key on the SCALE tier (`scale_tier_required` otherwise;
        max 5 active subscriptions per key — `too_many_webhooks`). Omit `mints`
        to alert on EVERY rugged token, or list specific mints. Deliveries are
        POSTed by a worker (~5 min cadence) with headers `X-Thunder-Event:
        rugged` and `X-Thunder-Signature: t=<unix_seconds>,v1=<hex>` where
        v1 = HMAC_SHA256(secret, "{t}.{rawBody}") — verify v1 and reject stale
        t. Payload: { event, mint, token:{name,symbol}, riskScore, verdict,
        ruggedAt, reason, deliveryId, attempt }. Retries: 5m/30m/2h/6h, then
        failed; subscriptions auto-disable after 25 consecutive failures.
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  description: Absolute http(s) URL to POST alerts to
                mints:
                  type: array
                  items: { type: string }
                  description: Optional mint filter (omit = all rugged tokens)
      responses:
        "201":
          description: Subscription created (includes the signing secret)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, enum: [true] }
                  data: { $ref: "#/components/schemas/WebhookSubscription" }
        "400":
          description: Invalid body (`invalid_request`) or subscription cap reached (`too_many_webhooks`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401":
          description: Missing (`key_required`) or invalid (`invalid_key`) API key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403":
          description: Missing scope (`missing_scope`) or non-Scale tier (`scale_tier_required`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
    get:
      tags: [RugCheck]
      operationId: listWebhooks
      summary: List this key's webhook subscriptions (Scale tier)
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      responses:
        "200":
          description: Subscriptions (secrets included — retrievable model)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, enum: [true] }
                  data:
                    type: object
                    properties:
                      webhooks:
                        type: array
                        items: { $ref: "#/components/schemas/WebhookSubscription" }
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "403":
          description: Missing scope or non-Scale tier
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /api/v1/webhooks/{id}:
    delete:
      tags: [RugCheck]
      operationId: deleteWebhook
      summary: Delete a webhook subscription
      description: Allowed even after the Scale tier lapses — cleanup is never gated.
      security:
        - ApiKeyHeader: []
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, enum: [true] }
                  message: { type: string }
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: Not found for this key (`not_found`)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /api/v1/risk/{mint}:
    get:
      tags: [RugCheck]
      operationId: getRiskScore
      summary: Lightweight risk score for a Solana mint
      description: >
        Same engine and cache as the full report, ~50x smaller response — for
        high-volume programmatic use (snipe filters, trading bots). Note this
        returns the RISK score (higher = riskier). Recommended fail-closed
        pattern: treat any non-2xx, `rugged: true`, or non-empty `flags` as
        "do not trade".
      parameters:
        - $ref: "#/components/parameters/Mint"
      responses:
        "200":
          description: Risk score
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: "#/components/schemas/RiskScore"
        "400": { $ref: "#/components/responses/InvalidMint" }
        "401": { $ref: "#/components/responses/InvalidKey" }
        "403": { $ref: "#/components/responses/MissingScope" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ScanFailed" }
        "503": { $ref: "#/components/responses/Unavailable" }

components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key (sk_...) with the rugcheck:read scope
    BearerAuth:
      type: http
      scheme: bearer
      description: The same API key sent as a Bearer token

  parameters:
    Mint:
      name: mint
      in: path
      required: true
      description: Base58 Solana mint address
      schema:
        type: string
        example: DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263

  responses:
    InvalidMint:
      description: Not a valid base58 Solana address (`invalid_mint`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    InvalidKey:
      description: An API key was sent but is invalid or expired (`invalid_key`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    MissingScope:
      description: The API key lacks the rugcheck:read scope (`missing_scope`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: >
        Per-IP/per-key rate limit exceeded (`rate_limited`, includes
        retryAfterSeconds) or monthly fresh-scan quota reached
        (`quota_exceeded`, includes used/limit; cached reads still served)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ScanFailed:
      description: The core engine failed for this mint — retry later (`scan_failed`)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unavailable:
      description: >
        `disabled` (RugCheck switched off), `quota_unavailable` (quota service
        unreachable — fresh scans paused, cached reads served), or `capacity`
        (global daily fresh-scan cap exhausted — resets at UTC midnight)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      required: [success, error, message]
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          type: string
          description: Machine-readable code
          enum:
            - invalid_mint
            - invalid_wallet
            - invalid_request
            - invalid_key
            - key_required
            - missing_scope
            - paid_tier_required
            - scale_tier_required
            - batch_too_large
            - too_many_webhooks
            - not_found
            - internal_error
            - rate_limited
            - quota_exceeded
            - scan_failed
            - quota_unavailable
            - capacity
            - disabled
            - emergency
        message:
          type: string
        retryAfterSeconds:
          type: integer
          description: Present on rate_limited
        used:
          type: integer
          description: Present on quota_exceeded
        limit:
          type: integer
          description: Present on quota_exceeded

    WalletReputation:
      type: object
      properties:
        wallet: { type: string }
        priorLaunches: { type: integer }
        ruggedLaunches: { type: integer }
        rugRate:
          type: number
          description: ruggedLaunches / priorLaunches (0-1)
        isSerialRugger: { type: boolean }
        fundedBy:
          type: object
          nullable: true
          properties:
            funder: { type: string }
            sourceClass:
              type: string
              enum: [known_scammer, mixer, prior_rugger, fresh_cex, cex, established, unknown]
        profileUpdatedAt:
          type: string
          format: date-time
          nullable: true
        recentRuggedTokens:
          type: array
          items:
            type: object
            properties:
              mint: { type: string }
              name: { type: string, nullable: true }
              symbol: { type: string, nullable: true }
              ruggedAt: { type: string, format: date-time }

    MintHistory:
      type: object
      properties:
        mint: { type: string }
        token:
          type: object
          properties:
            name: { type: string, nullable: true }
            symbol: { type: string, nullable: true }
        summary:
          type: object
          properties:
            riskScore: { type: integer }
            verdict: { type: string }
            rugged: { type: boolean }
            ruggedAt: { type: string, format: date-time, nullable: true }
            ruggedReason: { type: string, nullable: true }
            firstScannedAt: { type: string, format: date-time }
            lastScannedAt: { type: string, format: date-time }
            scanCount: { type: integer }
            currentLiquidityUsd: { type: number, nullable: true }
            peakLiquidityUsd: { type: number, nullable: true }
            liquidityDrawdownPct:
              type: number
              nullable: true
              description: "% of peak liquidity that has left the pool (0 = at peak)"
        snapshots:
          type: array
          description: 10-min cadence, 30-day retention, oldest-first, max 500 points
          items:
            type: object
            properties:
              capturedAt: { type: string, format: date-time }
              holderCount: { type: integer, nullable: true }
              liquidityUsd: { type: number, nullable: true }
              priceUsd: { type: number, nullable: true }
              netFlowSol: { type: number, nullable: true }
              earlyBuyersHeldPct: { type: number, nullable: true }
              insiderClusterPct: { type: number, nullable: true }

    WebhookSubscription:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string }
        secret:
          type: string
          description: whsec_... HMAC signing secret (verify X-Thunder-Signature with it)
        events:
          type: array
          items: { type: string }
        mints:
          type: array
          items: { type: string }
          nullable: true
          description: Null = alerts for every rugged token
        is_active: { type: boolean }
        failure_count: { type: integer }
        last_delivery_at: { type: string, format: date-time, nullable: true }
        last_delivery_status: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    BatchItem:
      type: object
      required: [mint, ok]
      properties:
        mint: { type: string }
        ok: { type: boolean }
        data:
          description: Present when ok — RiskScore shape (default) or RugReport (full=true)
          oneOf:
            - $ref: "#/components/schemas/RiskScore"
            - $ref: "#/components/schemas/RugReport"
        error:
          type: string
          description: Present when not ok (invalid_mint, quota_exceeded, capacity, scan_failed, ...)
        message: { type: string }

    RiskLevel:
      type: string
      enum: [low, medium, high, critical]

    Verdict:
      type: string
      enum: [Safe, Caution, High Risk, Danger]

    RiskScore:
      type: object
      required: [mint, riskScore, riskLevel, verdict, flags, rugged]
      properties:
        mint: { type: string }
        riskScore:
          type: integer
          minimum: 0
          maximum: 100
          description: Higher = riskier
        riskLevel: { $ref: "#/components/schemas/RiskLevel" }
        verdict: { $ref: "#/components/schemas/Verdict" }
        flags:
          type: array
          items: { type: string }
          description: Hard-cap conditions with deterministic proof (honeypot, unlocked_lp, ...)
        rugged:
          type: boolean
          description: Terminal — a drained token stays Danger forever

    Finding:
      type: object
      required: [category, check, status, severity, evidence, explanation]
      properties:
        category: { type: string, description: "Report section, e.g. Authorities, Liquidity" }
        check: { type: string, description: "Short check name, e.g. LP Burned" }
        status:
          type: string
          enum: [pass, warn, fail, info]
        severity: { type: string }
        evidence: { type: string, description: Concrete on-chain evidence }
        explanation: { type: string, description: Plain-English meaning }

    RugReport:
      type: object
      required:
        - apiVersion
        - mint
        - scannedAt
        - cached
        - token
        - safetyScore
        - riskScore
        - riskLevel
        - verdict
        - summary
        - rugged
        - ruggedAt
        - flags
        - market
        - holders
        - creator
        - findings
        - dataAvailability
      properties:
        apiVersion:
          type: string
          enum: [v1]
        mint: { type: string }
        scannedAt:
          type: string
          format: date-time
        cached:
          type: boolean
          description: True when served from the report cache
        token:
          type: object
          required: [name, symbol, imageUrl, isNative]
          properties:
            name: { type: string, nullable: true }
            symbol: { type: string, nullable: true }
            imageUrl: { type: string, nullable: true }
            isNative:
              type: boolean
              description: Launched on ThunderLaunch
        safetyScore:
          type: integer
          minimum: 0
          maximum: 100
          description: Higher = safer (100 - riskScore)
        riskScore:
          type: integer
          minimum: 0
          maximum: 100
        riskLevel: { $ref: "#/components/schemas/RiskLevel" }
        verdict: { $ref: "#/components/schemas/Verdict" }
        summary: { type: string }
        rugged: { type: boolean }
        ruggedAt:
          type: string
          format: date-time
          nullable: true
        flags:
          type: array
          items: { type: string }
        market:
          type: object
          nullable: true
          description: Null = unavailable (see dataAvailability), never "passed"
          properties:
            priceUsd: { type: number, nullable: true }
            marketCap: { type: number, nullable: true }
            liquidityUsd: { type: number, nullable: true }
            volume24h: { type: number, nullable: true }
            pairAgeDays: { type: number, nullable: true }
        holders:
          type: object
          nullable: true
          properties:
            totalHolders: { type: integer }
            effectiveHolders:
              type: integer
              nullable: true
              description: Corrected for dust/airdrop inflation
            topHolderPct:
              type: number
              nullable: true
              description: Largest single non-burn holder's supply %
            devHoldingsPct: { type: number, nullable: true }
            insiderClusterPct:
              type: number
              nullable: true
              description: Supply % held by deployer-connected clusters
        creator:
          type: object
          nullable: true
          properties:
            deployer: { type: string, nullable: true }
            priorLaunches: { type: integer, nullable: true }
            ruggedLaunches: { type: integer, nullable: true }
            isSerialRugger: { type: boolean }
        findings:
          type: array
          items: { $ref: "#/components/schemas/Finding" }
        dataAvailability:
          type: object
          description: False = "unverifiable", never "passed"
          required: [market, holders, launchIntegrity, creator, manipulation, sellSimulation]
          properties:
            market: { type: boolean }
            holders: { type: boolean }
            launchIntegrity: { type: boolean }
            creator: { type: boolean }
            manipulation: { type: boolean }
            sellSimulation: { type: boolean }
