openapi: 3.0.3
info:
  title: AttaGo API
  version: "1.0.0"
  description: |
    AttaGo is a serverless multi-asset crypto trading dashboard that provides
    real-time Go/No-Go scoring for spot and perpetuals markets with tiered
    derivatives coverage.

    ## Agent Quick Start — Two Access Modes

    **Mode 1: x402 Pay-Per-Request (no account needed)**

    1. `GET /v1/agent/score?symbol=BTC` → receive HTTP 402
    2. Sign USDC payment with your wallet (EIP-712 or ed25519)
    3. Retry with `PAYMENT-SIGNATURE` header → receive business-tier data

    Pricing: $0.10/score, $0.45/full-data. Always business-tier (all futures tokens).
    Libraries: `@x402/fetch` (auto-handles 402 flow), `@x402/evm`, `@x402/svm`.

    **Mode 2: Registered Account (tier discounts + alerts + webhooks)**

    1. SignUp via Cognito API (programmatic, no browser needed)
    2. ConfirmSignUp with email verification code
    3. InitiateAuth (USER_PASSWORD_AUTH) → JWT tokens
    4. Create API key: `POST /v1/user/api-keys` with JWT → save the raw key
    5. Use `X-API-Key: ak_live_...` header on all subsequent requests
    6. Subscribe via `POST /v1/payments/subscribe` (x402-settled)

    API keys are the recommended auth for scripts and bots — no token refresh needed.

    Economics: ~600 x402 requests/month = $60 = Business subscription price.
    Business tier ($60/mo) includes 250 free pushes, 150 alerts, and webhooks.

    ## Tiered Futures Data

    Futures token coverage varies by subscription tier. AVAX is always included.

    | Tier | Futures Tokens | Spot Tokens |
    |------|----------------|-------------|
    | Free | Top 8 by volume | All |
    | Basic | Top 16 by volume | All |
    | Pro | Top 32 by volume | All |
    | Business | All tracked | All |

    x402 agent endpoints always return business-tier data (all futures tokens).
    Each asset includes a `derivSymbols` array listing covered futures tokens.

    ## Authentication

    Most endpoints accept either a Cognito JWT bearer token (`Authorization: Bearer {IdToken}`)
    or an API key (`X-API-Key: ak_live_...`). API keys are available to all paid tiers (Basic,
    Pro, Business) and are the recommended auth method for scripts, bots, and integrations.
    Create keys via `POST /v1/user/api-keys` (requires Cognito JWT for bootstrapping).
    Agents without an account can use x402 pay-per-request (no auth needed).

    ## x402 Payment Protocol

    Data-push and payment endpoints use the x402 protocol for USDC settlement:
    1. Request without `PAYMENT-SIGNATURE` header returns `402` with `PAYMENT-REQUIRED` header
    2. Client signs payment and includes `PAYMENT-SIGNATURE` header
    3. Server verifies and settles via PayAI facilitator
    4. Resource is served with `PAYMENT-RESPONSE` header

    Supported chains: Base (eip155:8453), Avalanche (eip155:43114), Polygon (eip155:137),
    Arbitrum (eip155:42161), Optimism (eip155:10), Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp).

    ### x402 Pricing — Agent Endpoints (no account required)

    | Endpoint | Price | Description |
    |----------|-------|-------------|
    | `GET /v1/agent/score?symbol={SYM}` | $0.10 USDC | Derived scoring data for a single asset (mirrors `/v1/api/data/{token}`) |
    | `GET /v1/agent/data` | $0.45 USDC | Derived scoring data for all tracked assets (~38 assets) |

    Agent endpoints require no Cognito account — payment IS authentication.
    Data refreshes every 30s (business-tier cadence).

    ### x402 Pricing — Data Push (registered users, after bundled credits)

    | Endpoint | Price | Description |
    |----------|-------|-------------|
    | `GET /v1/api/data/{token}` | $0.10 USDC | Derived scoring data for a single token |

    Data-push consumption order: included monthly pushes → prepaid bundle credits → x402 per-request ($0.10).

    ### Prepaid Bundles (registered users, Pro+ tiers)

    | Bundle | Price | Credits | Per-push |
    |--------|-------|---------|----------|
    | Small | $5 USDC | 60 | $0.08 |
    | Medium | $25 USDC | 350 | $0.07 |
    | Large | $50 USDC | 800 | $0.06 |

    ## Subscription Tiers

    | Tier | Price | Max Subs | Push Devices | Webhooks | API |
    |------|-------|----------|--------------|----------|-----|
    | Free | $0 | 0 | 0 | 0 | No |
    | Basic | $5/mo | 5 | 3 | 0 | Yes |
    | Pro | $20/mo | 25 | 5 | 3 | Yes |
    | Business | $60/mo | 150 | 10 | 10 | Yes |

    Alert cooldown is configurable per subscription (0–1440 min). 0 = realtime (no suppression).
    Annual billing available at 15% discount.

    ## Derived Data Model

    All paid API responses return **derived scoring data** — no raw provider metrics are exposed.
    Composite score is normalized (0-100) and not strictly additive of sub-scores.
    Component lists may evolve across schema versions.
  contact:
    name: AttaGo
    url: https://attago.bid
  license:
    name: Proprietary

# Server URL is overridden by auth-config.js at runtime in Scalar UI.
# This default is the production API for agents reading the raw YAML.
servers:
  - url: "https://api.attago.bid"
    description: "Production API (staging: api-staging.attago.io)"

x-x402-discovery: https://attago.bid/.well-known/x402.json

tags:
  - name: Agent API
    description: "x402-only endpoints for AI agents (no Cognito required). Always returns business-tier data (all futures tokens). Agents can also register via Cognito API for subscription-based access with tier discounts."
  - name: API Keys
    description: Create and manage API keys for programmatic access. Requires Cognito JWT (keys are the bootstrap output, not the bootstrap input). Available to Basic, Pro, and Business tiers.
  - name: Bundles
    description: Prepaid data-push bundles
  - name: Data
    description: Auth-gated dashboard data
  - name: Data Push
    description: x402-gated real-time scoring data
  - name: Payments
    description: x402 USDC subscription billing
  - name: Push Notifications
    description: Web Push subscription management
  - name: Subscriptions
    description: Alert subscription CRUD and catalog
  - name: User Profile
    description: User profile management
  - name: Wallets
    description: Wallet registration and verification
  - name: Messaging
    description: |
      Connect external messaging platforms (Telegram, more coming) to receive alert
      notifications via DM. Pro+ tier required.

      **How it works:**
      1. Send `/start` to the AttaGo bot on Telegram — receive a 6-digit link code.
      2. Submit the code via `POST /v1/user/messaging/telegram/link` — account linked.
      3. Alerts are delivered to your DM alongside email/push/webhook channels.
      4. Use `POST /v1/user/messaging/test` to send a test message.
      5. Disconnect anytime via `DELETE /v1/user/messaging/telegram`.

      **DM-only:** The bot rejects group chats and channels. One Telegram account per user.
  - name: Webhooks
    description: |
      Register HTTPS endpoints to receive real-time alert notifications via HTTP POST.

      **How it works:**
      1. Register a webhook URL (HTTPS only) via `POST /v1/user/webhooks` — you receive a signing secret (shown once, save it).
      2. When an alert fires or clears, AttaGo POSTs a **v2 callback payload** to your URL.
      3. Verify authenticity using the `X-AttaGo-Signature` header (HMAC-SHA256 of the raw JSON body).
      4. Fetch full scoring data from `data.url` (72-hour snapshot, no auth required).

      **Delivery headers:**

      | Header | Value |
      |--------|-------|
      | `Content-Type` | `application/json` |
      | `X-AttaGo-Signature` | HMAC-SHA256 hex digest of the raw JSON body |
      | `X-AttaGo-Event` | `alert` |
      | `User-Agent` | `AttaGo-Webhook/1.0` |

      **Delivery payload (v2 — slim callback design):**

      The webhook payload is intentionally minimal. It identifies *which* alert fired and provides
      a stable callback URL to fetch the exact scoring data that was live at alert-fire time.
      No inline metric values are included — fetch them from `data.url`.

      ```json
      {
        "event": "alert",
        "version": "2",
        "environment": "production",
        "alert": {
          "id": "BTC#spot_score",
          "label": "BTC spot bullish",
          "token": "BTC",
          "state": "triggered"
        },
        "data": {
          "url": "https://api.attago.bid/v1/data/push/2026-03-04T12-00-00-000Z-a1b2c3d4",
          "expiresAt": "2026-03-07T12:00:00.000Z"
        },
        "timestamp": "2026-03-04T12:00:00.000Z"
      }
      ```

      **Payload fields:**

      | Field | Type | Description |
      |-------|------|-------------|
      | `event` | string | Always `"alert"` |
      | `version` | string | Payload version — currently `"2"` |
      | `environment` | string | `"production"`, `"staging"`, or `"dev"` |
      | `alert.id` | string | Subscription ID (format: `{tokenId}#{metricName}`) |
      | `alert.label` | string | User's custom alert label, or `"{token} alert"` if none set |
      | `alert.token` | string | Token symbol (e.g. `"BTC"`) |
      | `alert.state` | string | `"triggered"` or `"resolved"` |
      | `data.url` | string or null | Callback URL to fetch full scoring data (72h TTL). `null` if snapshot creation failed. |
      | `data.expiresAt` | string | ISO 8601 timestamp — when the snapshot expires (~72h from alert time) |
      | `data.fallbackUrl` | string | Only present when `data.url` is null. Points to `https://{domain}/data/latest.json`. |
      | `timestamp` | string | ISO 8601 timestamp of when the alert fired |

      **Fetching alert data from `data.url`:**

      The `data.url` is a `GET /v1/data/push/{requestId}` endpoint that returns the full derived
      scoring data that was live when the alert fired. **No authentication required** — the requestId
      acts as a bearer token. The snapshot is available for 72 hours.

      ```
      GET https://api.attago.bid/v1/data/push/2026-03-04T12-00-00-000Z-a1b2c3d4
      → 200 OK { requestId, tokenId, createdAt, data: { token, composite, spot, perps, context, ... } }
      → 410 Gone (after 72 hours)
      ```

      **Resolved alerts:** When alert conditions are no longer met, a second webhook fires with
      `alert.state: "resolved"`. The `data.url` points to the snapshot at resolve-time.
      When the criteria are met again, a new `"triggered"` webhook will be sent.

      **Verifying signatures (Node.js example):**
      ```javascript
      import { createHmac } from "node:crypto";

      function verifyWebhook(rawBody, signature, secret) {
        const expected = createHmac("sha256", secret)
          .update(rawBody)
          .digest("hex");
        return signature === expected;
      }

      // In your HTTP handler:
      const sig = req.headers["x-attago-signature"];
      if (!verifyWebhook(rawBody, sig, WEBHOOK_SECRET)) {
        return res.status(401).json({ error: "Invalid signature" });
      }
      const payload = JSON.parse(rawBody);
      console.log(payload.alert.label, payload.alert.state);
      // → "BTC spot bullish" "triggered"
      ```

      **Verifying signatures (Python example):**
      ```python
      import hmac, hashlib

      def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
          expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
          return hmac.compare_digest(signature, expected)
      ```

      **Retry policy:** 3 retries with exponential backoff (1s, 4s, 16s). 2xx = success, 4xx (except 429) = permanent failure, 429/5xx = retryable. 5-second timeout per attempt.

      **URL validation:** Only HTTPS URLs with public domain hostnames are accepted. IP addresses, localhost, private ranges (10.x, 172.16-31.x, 192.168.x), and AWS metadata endpoints are rejected.

      **Tier limits:** Pro: 3 webhooks, Business: 10 webhooks. Free and Basic tiers cannot register webhooks.

security:
  - CognitoAuth: []
  - ApiKeyAuth: []

paths:
  # ── User Profile ──────────────────────────────────────────────────────────
  /v1/user/profile:
    get:
      tags: [User Profile]
      summary: Get user profile
      description: |
        Returns the authenticated user's profile. Auto-provisions on first access
        from Cognito JWT claims (email, userId). New users start on the free tier.
      operationId: getProfile
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserProfile"
              example:
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                email: "trader@example.com"
                planTier: pro
                role: user
                effectiveTier: pro
                arenaUsername: "CryptoTrader42"
                deliveryPreference: "email,push"
                subscriptionExpiresAt: "2026-04-03T12:00:00.000Z"
                billingCycle: monthly
                wallets:
                  - userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
                    chain: base
                    verifiedAt: "2026-02-20T10:30:00.000Z"
                createdAt: "2026-01-15T08:00:00.000Z"
                updatedAt: "2026-03-01T14:22:00.000Z"
        "201":
          description: Profile auto-provisioned on first access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserProfile"
              example:
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                email: "newuser@example.com"
                planTier: free
                role: user
                effectiveTier: free
                deliveryPreference: email
                createdAt: "2026-03-04T12:00:00.000Z"
                updatedAt: "2026-03-04T12:00:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"

    put:
      tags: [User Profile]
      summary: Update user profile
      description: |
        Update mutable profile fields. Only whitelisted fields can be modified.
        Protected fields (userId, email, planTier) cannot be changed via this endpoint.
      operationId: updateProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                arenaUsername:
                  type: string
                  maxLength: 30
                  pattern: "^[a-zA-Z0-9_-]*$"
                  description: Arena username (alphanumeric, hyphens, underscores)
                  example: Trader_Pro-99
                deliveryPreference:
                  type: string
                  enum: [email, push, webhook, both, all, "email,push", "email,webhook", "push,webhook", "email,push,webhook"]
                  description: |
                    Notification delivery channels. Accepts legacy single-word values or comma-separated combinations.
                    Legacy aliases: `both` = email+push, `all` = email+push+webhook.
                    Explicit combos: `email,push`, `email,webhook`, `push,webhook`, `email,push,webhook`.
                  example: email
      responses:
        "200":
          description: Updated profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserProfile"
              example:
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                email: "trader@example.com"
                planTier: pro
                role: user
                effectiveTier: pro
                arenaUsername: "Trader_Pro-99"
                deliveryPreference: "email,push"
                createdAt: "2026-01-15T08:00:00.000Z"
                updatedAt: "2026-03-04T12:05:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Subscriptions ─────────────────────────────────────────────────────────
  /v1/subscriptions/catalog:
    get:
      tags: [Subscriptions]
      summary: Get subscription catalog
      description: |
        Returns the available tokens and metrics for the user's tier.
        Free tier returns an empty catalog. Basic tier gets preset-only configs.
        Pro and Business tiers get full custom threshold support.
      operationId: getCatalog
      responses:
        "200":
          description: Tier-gated catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Catalog"
              examples:
                paid_tier:
                  summary: Pro tier catalog (custom thresholds)
                  value:
                    tokens:
                      - id: BTC
                        symbol: BTC
                        name: Bitcoin
                        image: "https://attago.bid/images/tokens/btc.png"
                      - id: ETH
                        symbol: ETH
                        name: Ethereum
                        image: "https://attago.bid/images/tokens/eth.png"
                      - id: SOL
                        symbol: SOL
                        name: Solana
                        image: "https://attago.bid/images/tokens/sol.png"
                    metrics:
                      spot_score:
                        label: Spot Score
                        unit: ""
                        type: number
                        operators: [gt, lt, gte, lte, eq]
                        min: -50
                        max: 50
                      perps_score:
                        label: Perps Score
                        unit: ""
                        type: number
                        operators: [gt, lt, gte, lte, eq]
                        min: -50
                        max: 50
                      composite_score:
                        label: Composite Score
                        unit: ""
                        type: number
                        operators: [gt, lt, gte, lte, eq]
                        min: 0
                        max: 100
                      spot_decision:
                        label: Spot Decision
                        type: enum
                        values: [GO, STARTER, NO-GO]
                        operators: [eq]
                      funding_rate:
                        label: Funding Rate
                        unit: "%"
                        type: number
                        operators: [gt, lt, gte, lte]
                        min: -1
                        max: 1
                    tier: pro
                    maxSubscriptions: 25
                free_tier:
                  summary: Free tier (empty catalog)
                  value:
                    tokens: []
                    metrics: {}
                    tier: free
                    maxSubscriptions: 0
                    mode: null
                    message: "Free tier does not have subscriptions. Upgrade to Basic or higher."
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/subscriptions:
    get:
      tags: [Subscriptions]
      summary: List subscriptions
      description: |
        Returns alert subscriptions for the authenticated user.
        Excludes lifecycle-disabled subscriptions (tier_insufficient, token_delisted)
        which are permanently disabled when the user's tier no longer supports a token.
      operationId: listSubscriptions
      responses:
        "200":
          description: List of subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Subscription"
              example:
                subscriptions:
                  - userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    subId: "f7e6d5c4-b3a2-9180-7654-321fedcba098"
                    tokenId: BTC
                    label: "BTC spot bullish"
                    groups:
                      - - metricName: spot_score
                          thresholdOp: gt
                          thresholdVal: 5
                    cooldownMinutes: 5
                    bucketHash: "a3f8c9d1e2b04567"
                    isActive: true
                    createdAt: "2026-02-15T10:00:00.000Z"
                    updatedAt: "2026-02-15T10:00:00.000Z"
                  - userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    subId: "e8d7c6b5-a4f3-8270-6543-210fedcba987"
                    tokenId: ETH
                    label: "ETH funding extreme"
                    groups:
                      - - metricName: funding_rate
                          thresholdOp: gt
                          thresholdVal: 0.05
                        - metricName: perps_score
                          thresholdOp: gt
                          thresholdVal: 10
                    cooldownMinutes: 15
                    bucketHash: "b4e9d0a2f3c15678"
                    isActive: true
                    createdAt: "2026-02-20T14:30:00.000Z"
                    updatedAt: "2026-03-01T09:15:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      tags: [Subscriptions]
      summary: Create subscription
      description: |
        Create a new alert subscription. One subscription per token-metric pair.
        Subject to tier limits (Basic: 5, Pro: 25, Business: 150).
      operationId: createSubscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSubscriptionRequest"
            examples:
              single_condition:
                summary: Single condition (flat format)
                value:
                  tokenId: BTC
                  label: "BTC spot bullish"
                  metricName: spot_score
                  thresholdOp: gt
                  thresholdVal: 5
                  cooldownMinutes: 5
              multi_condition_and:
                summary: Multi-condition AND group
                value:
                  tokenId: ETH
                  label: "ETH funding extreme + high score"
                  groups:
                    - - metricName: funding_rate
                        thresholdOp: gt
                        thresholdVal: 0.05
                      - metricName: perps_score
                        thresholdOp: gt
                        thresholdVal: 10
                  cooldownMinutes: 15
              or_of_ands:
                summary: OR-of-ANDs (trigger on either condition group)
                value:
                  tokenId: SOL
                  label: "SOL breakout or breakdown"
                  groups:
                    - - metricName: composite_score
                        thresholdOp: gt
                        thresholdVal: 80
                    - - metricName: composite_score
                        thresholdOp: lt
                        thresholdVal: 20
                  cooldownMinutes: 30
      responses:
        "201":
          description: Subscription created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
              example:
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                subId: "d9c8b7a6-5f4e-3d2c-1b0a-9876543210fe"
                tokenId: SOL
                label: "SOL composite breakout"
                groups:
                  - - metricName: composite_score
                      thresholdOp: gt
                      thresholdVal: 75
                cooldownMinutes: 10
                bucketHash: "c5f0e1b3a4d26789"
                isActive: true
                activeTokenShard: "SOL#3"
                createdAt: "2026-03-04T12:00:00.000Z"
                updatedAt: "2026-03-04T12:00:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Tier limit reached or free tier
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Subscription already exists for this token-metric pair
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/user/subscriptions/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: "Subscription ID (format: {tokenId}#{metricName}, URL-encoded)"
        schema:
          type: string
        example: "BTC%23spot_score"

    put:
      tags: [Subscriptions]
      summary: Update subscription
      description: |
        Update an existing subscription. Updatable fields: isActive, label, groups, cooldownMinutes.
        Group/condition changes are re-validated against the catalog.
        Re-enabling a lifecycle-disabled subscription (tier_insufficient or token_delisted) returns 403.
      operationId: updateSubscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                isActive:
                  type: boolean
                  description: Enable or disable the subscription
                label:
                  type: string
                  maxLength: 100
                  description: Update subscription label
                groups:
                  type: array
                  description: "Replace condition groups (OR-of-ANDs format)"
                  items:
                    type: array
                    items:
                      type: object
                      required: [metricName, thresholdOp, thresholdVal]
                      properties:
                        metricName:
                          type: string
                        thresholdOp:
                          type: string
                          enum: [gt, lt, gte, lte, eq]
                        thresholdVal:
                          oneOf:
                            - type: number
                            - type: string
                cooldownMinutes:
                  type: integer
                  minimum: 0
                  maximum: 1440
                  description: "Update alert cooldown (0 = realtime, min 1, max 1440)"
            examples:
              update_threshold:
                summary: Update condition threshold and cooldown
                value:
                  groups:
                    - - metricName: composite_score
                        thresholdOp: gt
                        thresholdVal: 80
                  cooldownMinutes: 30
              disable_alert:
                summary: Disable an alert subscription
                value:
                  isActive: false
              rename_alert:
                summary: Update label only
                value:
                  label: "BTC spot bullish (updated)"
      responses:
        "200":
          description: Updated subscription
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
              example:
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                subId: "d9c8b7a6-5f4e-3d2c-1b0a-9876543210fe"
                tokenId: SOL
                label: "SOL composite breakout (updated)"
                groups:
                  - - metricName: composite_score
                      thresholdOp: gt
                      thresholdVal: 80
                cooldownMinutes: 30
                bucketHash: "d6a1f2c4b5e37890"
                isActive: true
                activeTokenShard: "SOL#3"
                createdAt: "2026-03-04T12:00:00.000Z"
                updatedAt: "2026-03-04T12:30:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Cannot re-enable a lifecycle-disabled subscription (tier_insufficient or token_delisted)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    delete:
      tags: [Subscriptions]
      summary: Delete subscription
      description: Permanently deletes an alert subscription.
      operationId: deleteSubscription
      responses:
        "200":
          description: Subscription deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
                  subscription:
                    $ref: "#/components/schemas/Subscription"
              example:
                deleted: true
                subscription:
                  userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  subId: "d9c8b7a6-5f4e-3d2c-1b0a-9876543210fe"
                  tokenId: SOL
                  label: "SOL composite breakout"
                  groups:
                    - - metricName: composite_score
                        thresholdOp: gt
                        thresholdVal: 75
                  cooldownMinutes: 10
                  bucketHash: "c5f0e1b3a4d26789"
                  isActive: false
                  createdAt: "2026-03-04T12:00:00.000Z"
                  updatedAt: "2026-03-04T13:00:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Push Notifications ───────────────────────────────────────────────────
  /v1/user/vapid-key:
    get:
      tags: [Push Notifications]
      summary: Get VAPID public key
      description: Returns the VAPID public key needed for Web Push subscription.
      operationId: getVapidKey
      responses:
        "200":
          description: VAPID public key
          content:
            application/json:
              schema:
                type: object
                properties:
                  vapidPublicKey:
                    type: string
                    description: Base64url-encoded VAPID public key
              example:
                vapidPublicKey: "BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkOs-qy014e0_B-OPn7kJv0vSaFkzJx-6l-DqS9Cg"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/push-subscriptions:
    post:
      tags: [Push Notifications]
      summary: Register push subscription
      description: |
        Register a Web Push subscription for the authenticated user.
        Subject to tier limits (Basic: 3, Pro: 5, Business: 10 devices).
        Free tier cannot register push subscriptions.
        Deterministic subscriptionId from endpoint hash (re-registering = upsert).
      operationId: createPushSubscription
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [endpoint, keys]
              properties:
                endpoint:
                  type: string
                  format: uri
                  description: Push service endpoint URL (HTTPS only)
                keys:
                  type: object
                  required: [p256dh, auth]
                  properties:
                    p256dh:
                      type: string
                      description: Client public key for push encryption
                    auth:
                      type: string
                      description: Client auth secret for push encryption
            example:
              endpoint: "https://fcm.googleapis.com/fcm/send/eK9R-abc123..."
              keys:
                p256dh: "BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkOs-qy014e0_B-OPn7kJv0vSaFkzJx-6l-DqS9Cg"
                auth: "xY8dRt5Z6F-mQp2k_abc12"
      responses:
        "201":
          description: Subscription created
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptionId:
                    type: string
                    description: Deterministic ID (SHA-256 hash of endpoint, 16 chars)
                  endpoint:
                    type: string
                  createdAt:
                    type: string
                    format: date-time
              example:
                subscriptionId: "a3f8c9d1e2b04567"
                endpoint: "https://fcm.googleapis.com/fcm/send/eK9R-abc123..."
                createdAt: "2026-03-04T12:00:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

    get:
      tags: [Push Notifications]
      summary: List push subscriptions
      description: Returns all push subscriptions for the authenticated user.
      operationId: listPushSubscriptions
      responses:
        "200":
          description: List of push subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        subscriptionId:
                          type: string
                        endpoint:
                          type: string
                        createdAt:
                          type: string
                          format: date-time
              example:
                items:
                  - subscriptionId: "a3f8c9d1e2b04567"
                    endpoint: "https://fcm.googleapis.com/fcm/send/eK9R-abc123..."
                    createdAt: "2026-02-15T10:00:00.000Z"
                  - subscriptionId: "b4e9d0a2f3c15678"
                    endpoint: "https://updates.push.services.mozilla.com/wpush/v2/gAA..."
                    createdAt: "2026-03-01T14:30:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/push-subscriptions/{id}:
    delete:
      tags: [Push Notifications]
      summary: Delete push subscription
      description: Remove a push subscription by its ID.
      operationId: deletePushSubscription
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Subscription ID (16-char hex hash of endpoint)
      responses:
        "200":
          description: Subscription deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
              example:
                deleted: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Webhooks ────────────────────────────────────────────────────────────
  /v1/user/webhooks:
    post:
      tags: [Webhooks]
      summary: Register webhook
      description: |
        Register a webhook endpoint for the authenticated user.
        Requires Pro or Business tier (apiAccess). Subject to tier limits (Pro: 3, Business: 10).
        Returns the webhook signing secret only on creation — list endpoint strips it.
        All deliveries are signed with HMAC-SHA256 using the webhook secret.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: Webhook endpoint URL (HTTPS only)
            example:
              url: "https://example.com/webhook"
      responses:
        "201":
          description: Webhook created (secret shown only here)
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhookId:
                    type: string
                    format: uuid
                  url:
                    type: string
                  secret:
                    type: string
                    description: HMAC-SHA256 signing secret (shown only on creation)
                  createdAt:
                    type: string
                    format: date-time
              example:
                webhookId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                url: "https://example.com/webhook"
                secret: "wh_testKey123NotReal456Example789"
                createdAt: "2026-02-26T12:00:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

    get:
      tags: [Webhooks]
      summary: List webhooks
      description: Returns all webhooks for the authenticated user. Secrets are stripped from the response.
      operationId: listWebhooks
      responses:
        "200":
          description: List of webhooks (secrets excluded)
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        webhookId:
                          type: string
                          format: uuid
                        url:
                          type: string
                        createdAt:
                          type: string
                          format: date-time
              example:
                items:
                  - webhookId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    url: "https://example.com/webhook"
                    createdAt: "2026-02-26T12:00:00.000Z"
                  - webhookId: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                    url: "https://bot.example.com/alerts"
                    createdAt: "2026-02-26T14:30:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Delete webhook
      description: Remove a webhook by its ID. Alert deliveries will no longer be sent to this endpoint.
      operationId: deleteWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook ID
      responses:
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
              example:
                deleted: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Test webhook delivery
      description: |
        Trigger a server-side test delivery for a registered webhook.
        The server looks up the webhook, builds a test payload (`event: "test"`),
        HMAC-signs it with the stored secret, and delivers through the production
        retry pipeline. Returns the delivery result.

        Requires Pro or Business tier (same as webhook creation).
      operationId: testWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Webhook ID
      responses:
        "200":
          description: Delivery result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    description: Whether the receiver responded with 2xx
                  statusCode:
                    type: integer
                    nullable: true
                    description: HTTP status code from the receiver (null if network error)
                  attempts:
                    type: integer
                    description: Total delivery attempts (1-4)
                  error:
                    type: string
                    description: Error message if delivery failed
              example:
                success: true
                statusCode: 200
                attempts: 1
        "403":
          description: Tier too low (requires Pro or Business)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Webhooks require Pro or Business tier"
        "404":
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Webhook not found"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Messaging ────────────────────────────────────────────────────────────
  /v1/user/messaging:
    get:
      tags: [Messaging]
      summary: List connected messaging platforms
      description: |
        Returns all messaging platforms linked to the authenticated user's account.
      operationId: listMessagingLinks
      security:
        - CognitoAuth: []
      responses:
        "200":
          description: List of connected platforms
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/MessagingLink"
              example:
                items:
                  - userId: "abc-123"
                    platform: "telegram"
                    chatId: "987654321"
                    username: "johndoe"
                    linkedAt: "2026-03-26T20:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/messaging/telegram/link:
    post:
      tags: [Messaging]
      summary: Link Telegram account
      description: |
        Submit the 6-digit code received from the AttaGo Telegram bot to link
        your Telegram account. The code expires after 10 minutes.

        Requires Pro or Business tier.
      operationId: linkTelegram
      security:
        - CognitoAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: 6-character alphanumeric code from the bot
                  pattern: "^[A-Z0-9]{6}$"
                  example: "MX24BD"
      responses:
        "200":
          description: Telegram account linked
          content:
            application/json:
              schema:
                type: object
                properties:
                  linked:
                    type: boolean
                    example: true
                  username:
                    type: string
                    example: "johndoe"
        "400":
          description: Invalid or expired code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                badLength:
                  value:
                    error: "Code must be exactly 6 characters"
                expired:
                  value:
                    error: "Invalid or expired code"
        "403":
          description: Tier too low
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Pro or higher tier required to link messaging platforms"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/messaging/telegram:
    delete:
      tags: [Messaging]
      summary: Disconnect Telegram
      description: |
        Remove the linked Telegram account. Alerts will no longer be sent via Telegram.
      operationId: unlinkTelegram
      security:
        - CognitoAuth: []
      responses:
        "200":
          description: Telegram disconnected
          content:
            application/json:
              schema:
                type: object
                properties:
                  unlinked:
                    type: boolean
                    example: true
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/messaging/test:
    post:
      tags: [Messaging]
      summary: Send test message
      description: |
        Send a test alert message to all connected messaging platforms. Subject to
        a 30-second cooldown per Lambda instance.

        Requires Pro or Business tier.
      operationId: testMessaging
      security:
        - CognitoAuth: []
      responses:
        "200":
          description: Test results per platform
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: object
                    properties:
                      telegram:
                        type: object
                        properties:
                          success:
                            type: boolean
                          error:
                            type: string
                            nullable: true
              example:
                results:
                  telegram:
                    success: true
                    error: null
        "400":
          description: No platforms connected
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "No messaging platforms connected"
        "403":
          description: Tier too low
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Pro or higher tier required"
        "429":
          description: Cooldown active
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Please wait 25s before testing again"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── API Keys ─────────────────────────────────────────────────────────────
  /v1/user/api-keys:
    post:
      tags: [API Keys]
      summary: Create API key
      description: |
        Generate a new API key for programmatic access. The raw key is returned
        **once** in the response — save it immediately. Only the SHA-256 hash is
        stored; the key cannot be recovered.

        Requires Cognito JWT (not API key auth) — this is the bootstrap endpoint.
        Max 5 keys per user. Only paid tiers (Basic, Pro, Business) with
        `apiAccess: true` can create keys.
      operationId: createApiKey
      security:
        - CognitoAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Descriptive label for the key (alphanumeric, hyphens, underscores, max 64 chars)
                  pattern: "^[a-zA-Z0-9_-]+$"
                  maxLength: 64
                  example: my-trading-bot
            example:
              name: my-trading-bot
      responses:
        "201":
          description: API key created (raw key shown once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  keyId:
                    type: string
                    format: uuid
                  name:
                    type: string
                  prefix:
                    type: string
                    description: First 12 characters of the key (for identification)
                  rawKey:
                    type: string
                    description: Full API key — shown ONCE, save it now
                  createdAt:
                    type: string
                    format: date-time
              example:
                keyId: "c3d4e5f6-a7b8-9012-cdef-345678901234"
                name: my-trading-bot
                prefix: ak_live_a1b2
                rawKey: "ak_live_a1b2...save_this_key_it_wont_be_shown_again"
                createdAt: "2026-03-07T10:00:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Tier does not support API keys (free tier)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Your plan does not include API access"
        "409":
          description: Maximum API keys reached (5 per user)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Maximum of 5 API keys allowed"
    get:
      tags: [API Keys]
      summary: List API keys
      description: |
        Returns all API keys for the authenticated user (active and revoked).
        Only metadata is returned — never the raw key or hash.
      operationId: listApiKeys
      security:
        - CognitoAuth: []
      responses:
        "200":
          description: List of API keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object
                      properties:
                        keyId:
                          type: string
                          format: uuid
                        name:
                          type: string
                        prefix:
                          type: string
                        createdAt:
                          type: string
                          format: date-time
                        lastUsedAt:
                          type: string
                          format: date-time
                          nullable: true
                        revokedAt:
                          type: string
                          format: date-time
                          nullable: true
              example:
                keys:
                  - keyId: "c3d4e5f6-a7b8-9012-cdef-345678901234"
                    name: my-trading-bot
                    prefix: ak_live_a1b2
                    createdAt: "2026-03-07T10:00:00.000Z"
                    lastUsedAt: "2026-03-07T12:30:00.000Z"
                    revokedAt: null
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/user/api-keys/{id}:
    delete:
      tags: [API Keys]
      summary: Revoke API key
      description: |
        Soft-revoke an API key by setting `revokedAt`. The key immediately stops
        working but remains visible in `GET /v1/user/api-keys` for audit purposes.
        This action is irreversible.
      operationId: revokeApiKey
      security:
        - CognitoAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: API key ID
      responses:
        "200":
          description: API key revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean
              example:
                revoked: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: API key not found or does not belong to user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "API key not found"

  # ── Auth-Gated Data (Phase 9) ─────────────────────────────────────────────
  /v1/data/latest:
    get:
      tags: [Data]
      summary: Get latest dashboard data (paid tiers only)
      description: |
        Returns the latest dashboard data directly from S3, bypassing CloudFront cache.
        Paid data refreshes every 30s. Requires a paid tier (basic, pro, or business).
        Free users get 403 — use the CloudFront-cached `data/public.json` instead (5-min refresh).
        Respects tierOverride if set.
      operationId: getLatestData
      responses:
        "200":
          description: |
            Latest dashboard data (all assets, scoring, metadata).
            This is the raw tiered S3 data blob — the same shape that powers the
            dashboard frontend. Contains all tracked assets with full scoring,
            Fear & Greed index, and provider metadata. Response size varies by tier
            (business tier includes all futures tokens).
          content:
            application/json:
              schema:
                type: object
                properties:
                  assets:
                    type: object
                    description: Asset data keyed by symbol
                  assetOrder:
                    type: array
                    items:
                      type: string
                  meta:
                    type: object
                    description: Metadata (timestamp, version, providers)
              example:
                assets:
                  BTC:
                    symbol: BTC
                    name: Bitcoin
                    image: "https://attago.bid/images/tokens/btc.png"
                    spotScore: 42
                    perpsScore: 38
                    compositeScore: 72
                    signal: GO
                    confidence: 0.85
                  ETH:
                    symbol: ETH
                    name: Ethereum
                    image: "https://attago.bid/images/tokens/eth.png"
                    spotScore: 28
                    perpsScore: 22
                    compositeScore: 55
                    signal: NEUTRAL
                    confidence: 0.79
                assetOrder: [BTC, ETH, SOL, AVAX, BNB, XRP, ADA, DOGE]
                meta:
                  timestamp: "2026-03-04T12:00:00.000Z"
                  version: "2.0"
                  providers: [coinglass]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Paid tier required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Tier Override (Phase 9) ──────────────────────────────────────────────
  /v1/user/tier-override:
    put:
      tags: [User Profile]
      summary: Set own tier override (tester+ only)
      description: |
        Set a tier override on your own account. Requires tester, moderator, or admin role.
        The override affects all tier-gated features (GUI refresh, alert cooldowns, API access).
        Send `{ "tierOverride": null }` to clear.
      operationId: setOwnTierOverride
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tierOverride]
              properties:
                tierOverride:
                  type: string
                  enum: [free, basic, pro, business]
                  nullable: true
                  description: Tier to impersonate, or null to clear
            examples:
              set_override:
                summary: Impersonate business tier
                value:
                  tierOverride: business
              clear_override:
                summary: Clear tier override
                value:
                  tierOverride: null
      responses:
        "200":
          description: Updated profile with tierOverride
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserProfile"
              example:
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                email: "tester@example.com"
                planTier: free
                role: tester
                effectiveTier: business
                tierOverride: business
                createdAt: "2026-01-15T08:00:00.000Z"
                updatedAt: "2026-03-04T12:10:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Tester, moderator, or admin role required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Payments ──────────────────────────────────────────────────────────────
  /v1/payments/subscribe:
    post:
      tags: [Payments]
      summary: Subscribe to a tier (or renew)
      description: |
        Pay for a subscription tier via x402 USDC settlement.

        **Prerequisite**: At least one verified wallet must be registered via
        `POST /v1/payments/wallet` before subscribing. Returns 400 if no wallet
        is on file. Wallets are required for included push consumption, bundle
        deduction, and refund eligibility.

        **Renewal**: Set `renew: true` to extend an existing subscription from
        its current expiry date. Tier and billing cycle must match the current
        subscription. Renewals charge full price (no prorate).

        **Test mode**: Tier upgrade is instant (no payment required).

        **Live mode**: Requires `PAYMENT-SIGNATURE` header with x402 payment.
        Without the header, returns 402 with payment requirements.
      operationId: subscribe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tier]
              properties:
                tier:
                  type: string
                  enum: [basic, pro, business]
                  description: Target subscription tier
                billingCycle:
                  type: string
                  enum: [monthly, annual]
                  default: monthly
                  description: Billing cycle (annual gets 15% discount)
                renew:
                  type: boolean
                  default: false
                  description: Set to true to renew existing subscription. Must match current tier and cycle. Extends from current expiry date.
            examples:
              new_monthly:
                summary: New monthly Pro subscription
                value:
                  tier: pro
                  billingCycle: monthly
              annual_discount:
                summary: Annual Business subscription (15% off)
                value:
                  tier: business
                  billingCycle: annual
              renewal:
                summary: Renew existing subscription
                value:
                  tier: pro
                  billingCycle: monthly
                  renew: true
      responses:
        "200":
          description: Subscription activated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscriptionPaymentResponse"
              example:
                tier: pro
                billingCycle: monthly
                price: 20
                currency: USDC
                expiresAt: "2026-04-04T12:00:00.000Z"
                payer: "0xabcdef0123456789abcdef0123456789abcdef01"
                mode: live
                message: "Subscription activated: pro (monthly)"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "409":
          description: Payment already processed (replay protection)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/payments/status:
    get:
      tags: [Payments]
      summary: Get payment status
      description: |
        Returns the user's current subscription tier, billing info,
        and grace period status.
      operationId: getPaymentStatus
      responses:
        "200":
          description: Payment status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentStatus"
              examples:
                active_subscription:
                  summary: Active Pro subscription with included pushes
                  value:
                    tier: pro
                    tierName: Pro
                    billingCycle: monthly
                    expiresAt: "2026-04-04T12:00:00.000Z"
                    maxSubs: 25
                    apiAccess: true
                    freeDataPushes: 100
                    mode: live
                    includedPushes:
                      total: 100
                      used: 23
                      remaining: 77
                      periodStart: "2026-03-04T12:00:00.000Z"
                      periodEnd: "2026-04-03T12:00:00.000Z"
                free_tier:
                  summary: Free tier (no subscription)
                  value:
                    tier: free
                    tierName: Free
                    billingCycle: null
                    expiresAt: null
                    maxSubs: 0
                    apiAccess: false
                    freeDataPushes: 0
                    mode: live
                expired_grace:
                  summary: Expired subscription in grace period
                  value:
                    tier: pro
                    tierName: Pro
                    billingCycle: monthly
                    expiresAt: "2026-03-01T12:00:00.000Z"
                    maxSubs: 25
                    apiAccess: true
                    freeDataPushes: 100
                    mode: live
                    expired: true
                    graceDaysRemaining: 4
        "401":
          $ref: "#/components/responses/Unauthorized"


  # ── Wallets ───────────────────────────────────────────────────────────────
  /v1/payments/wallet:
    post:
      tags: [Wallets]
      summary: Register a wallet
      description: |
        Register a wallet address via challenge-response signature verification.
        Supports EVM (EIP-191 personal_sign) and Solana (ed25519) wallets.

        **Flow**:
        1. Client builds challenge: `AttaGo wallet verification\nUser: {userId}\nTimestamp: {unix_seconds}`
        2. Client signs with wallet
        3. POST signature + timestamp to this endpoint
        4. Server verifies and stores the wallet link

        Challenge expires after 5 minutes.
      operationId: registerWallet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterWalletRequest"
            examples:
              evm_wallet:
                summary: EVM wallet (Base chain)
                value:
                  walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
                  chain: base
                  signature: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01"
                  timestamp: 1709553600
              solana_wallet:
                summary: Solana wallet
                value:
                  walletAddress: "4AXQEiZYLTpYSHYLwu9XcS5wHuGGez3BrKuvkgNnvxtZ"
                  chain: solana
                  signature: "5wHuGGez3BrKuvkgNnvxtZabcdef0123456789abcdef0123456789abcdef012345"
                  timestamp: 1709553600
      responses:
        "201":
          description: Wallet registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WalletLink"
              example:
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
                chain: base
                verifiedAt: "2026-03-04T12:00:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Wallet already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/payments/wallets:
    get:
      tags: [Wallets]
      summary: List wallets
      description: Returns all verified wallet addresses for the authenticated user.
      operationId: listWallets
      responses:
        "200":
          description: List of wallets
          content:
            application/json:
              schema:
                type: object
                properties:
                  wallets:
                    type: array
                    items:
                      $ref: "#/components/schemas/WalletLink"
              example:
                wallets:
                  - userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
                    chain: base
                    verifiedAt: "2026-02-20T10:30:00.000Z"
                  - userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    walletAddress: "4AXQEiZYLTpYSHYLwu9XcS5wHuGGez3BrKuvkgNnvxtZ"
                    chain: solana
                    verifiedAt: "2026-03-01T14:00:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/payments/wallet/{address}:
    delete:
      tags: [Wallets]
      summary: Remove a wallet
      description: Removes a registered wallet address.
      operationId: removeWallet
      parameters:
        - name: address
          in: path
          required: true
          description: Wallet address to remove (URL-encoded)
          schema:
            type: string
          example: "0xabcdef0123456789abcdef0123456789abcdef01"
      responses:
        "200":
          description: Wallet removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
                  wallet:
                    $ref: "#/components/schemas/WalletLink"
              example:
                deleted: true
                wallet:
                  userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
                  chain: base
                  verifiedAt: "2026-02-20T10:30:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Wallet not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Data Push ─────────────────────────────────────────────────────────────
  /v1/api/data/{token}:
    get:
      tags: [Data Push]
      summary: Get derived scoring data for a token
      description: |
        Returns derived scoring data with categorical context for a single token — no raw provider metrics.
        Same data shape as `/v1/agent/score`, but designed for registered users with wallet-based
        bundle/subscription payment priority.

        **No Cognito authentication required** — payment IS the authentication.

        **Payment priority** (registered users with wallets):
        1. Included monthly pushes (from subscription tier) are consumed first
        2. Prepaid bundle credits are consumed next (requires `wallet` param + ownership proof)
        3. Falls back to x402 per-request payment ($0.10 USDC)

        **Bundle authentication**: When using `wallet` param, you must also provide
        `X-Bundle-Signature`, `X-Bundle-Timestamp`, and `X-Bundle-Chain` headers
        to prove wallet ownership (prevents unauthorized bundle drain).

        **Test mode**: Returns data without payment.
      operationId: getDataPush
      security: []
      x-x402:
        priceUsd: 0.10
        schemes: ["exact"]
        networks: ["eip155:8453", "eip155:43114", "eip155:137", "eip155:42161", "eip155:10", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"]
        asset: USDC
        note: "Prepaid bundle deduction is attempted first when wallet param is provided"
      parameters:
        - name: token
          in: path
          required: true
          description: Token symbol (uppercase, 2-10 chars)
          schema:
            type: string
            pattern: "^[A-Z]{2,10}$"
          example: BTC
        - name: wallet
          in: query
          required: false
          description: Wallet address for prepaid bundle deduction (requires ownership proof headers)
          schema:
            type: string
        - name: X-Bundle-Signature
          in: header
          required: false
          description: "Signed challenge proving wallet ownership (hex for EVM, base58 for Solana). Required when `wallet` param is provided."
          schema:
            type: string
        - name: X-Bundle-Timestamp
          in: header
          required: false
          description: "Unix timestamp (seconds) of the signed challenge. Must be within 5 minutes. Required when `wallet` param is provided."
          schema:
            type: integer
        - name: X-Bundle-Chain
          in: header
          required: false
          description: "Chain identifier. Required when `wallet` param is provided."
          schema:
            type: string
            enum: [base, avalanche, polygon, arbitrum, optimism, solana]
      responses:
        "200":
          description: Scoring data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataPushResponse"
              examples:
                bundle_deduction:
                  summary: Response using prepaid bundle credits
                  value:
                    token: BTC
                    composite:
                      score: 72
                      signal: GO
                      confidence: 0.85
                    spot:
                      score: 42
                      decision: GO
                      bias: Pumping
                      confidence: 0.82
                      components:
                        - name: Volume Confluence
                          score: 8.2
                          band: strong_positive
                        - name: Trend Alignment
                          score: 3.1
                          band: moderate_positive
                    perps:
                      score: 38
                      decision: GO
                      bias: Long
                      confidence: 0.88
                      squeezeRisk: High
                      components:
                        - name: Funding Pressure
                          score: -2.5
                          band: moderate_negative
                        - name: OI Momentum
                          score: 6.1
                          band: strong_positive
                    context:
                      funding: elevated
                      positioning: heavily_long
                      oiTrend: declining
                      basisSpread: normal
                      liquidationPressure: longs_exposed
                    market:
                      regime: Risk-On
                      btcDominance: 58.2
                      fearGreed: 72
                      altSeason: false
                    derivSymbols: [BTC, ETH, SOL, AVAX, BNB, XRP, ADA, DOGE]
                    hasDerivatives: true
                    sources:
                      - provider: Coinglass
                        api: v4
                        url: "https://open-api-v4.coinglass.com"
                    meta:
                      timestamp: "2026-03-04T12:00:00.000Z"
                      schemaVersion: "2.0"
                      signalId: a3f8c9d1e2b04567
                    requestId: "2026-03-04T12-00-00-000Z-a1b2c3d4"
                    mode: live
                    bundle:
                      bundleId: "c3d4e5f6-a7b8-9012-cdef-345678901234"
                      remaining: 42
                included_push:
                  summary: Response consuming an included monthly push
                  value:
                    token: ETH
                    composite:
                      score: 55
                      signal: NEUTRAL
                      confidence: 0.79
                    spot:
                      score: 28
                      decision: STARTER
                      bias: Chop
                      confidence: 0.74
                      components:
                        - name: Volume Confluence
                          score: 2.1
                          band: moderate_positive
                    perps:
                      score: 22
                      decision: STARTER
                      bias: Neutral
                      confidence: 0.83
                      squeezeRisk: Medium
                      components:
                        - name: Funding Pressure
                          score: 0.3
                          band: weak_positive
                    context:
                      funding: neutral
                      positioning: balanced
                      oiTrend: stable
                      basisSpread: normal
                      liquidationPressure: balanced
                    market:
                      regime: Risk-On
                      btcDominance: 58.2
                      fearGreed: 72
                      altSeason: false
                    derivSymbols: [BTC, ETH, SOL, AVAX]
                    hasDerivatives: true
                    sources:
                      - provider: Coinglass
                        api: v4
                        url: "https://open-api-v4.coinglass.com"
                    meta:
                      timestamp: "2026-03-04T12:00:30.000Z"
                      schemaVersion: "2.0"
                      signalId: b7e2d4f1a9c83061
                    requestId: "2026-03-04T12-00-30-000Z-d5e6f7a8"
                    mode: live
                    includedPush:
                      used: 24
                      total: 100
                      remaining: 76
        "400":
          $ref: "#/components/responses/BadRequest"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          description: No data for the requested token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Payment already processed (replay protection)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  # ── Data Push Snapshot Retrieval ──────────────────────────────────────────
  /v1/data/push/{requestId}:
    get:
      tags: [Data Push]
      summary: Retrieve a data-push snapshot
      description: |
        Retrieve a previously stored derived scoring snapshot by its requestId.
        **No authentication required** — the requestId acts as a bearer token.

        Snapshots are created in two ways:
        1. **Data-push payments** — every x402 per-request or bundle deduction response includes a `requestId`.
        2. **Alert-time snapshots** — when an alert fires or clears, the trigger Lambda stores a snapshot of the exact scoring data at that moment. Webhook payloads include this requestId in `data.url`.

        All snapshots are stored for **72 hours** with DynamoDB TTL auto-deletion.
        After expiry, this endpoint returns `410 Gone` with a `stale_data` error.

        **Webhook consumers**: Parse `data.url` from the webhook payload and fetch it to get the full
        derived scoring data that was live when the alert fired. No auth headers needed.
      operationId: getDataPushSnapshot
      security: []
      parameters:
        - name: requestId
          in: path
          required: true
          description: "Request ID returned from a data-push response (format: ISO-timestamp-UUID-prefix)"
          schema:
            type: string
          example: "2026-02-24T12-30-00-000Z-a1b2c3d4"
      responses:
        "200":
          description: Snapshot data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataPushSnapshot"
              example:
                requestId: "2026-03-04T12-00-00-000Z-a1b2c3d4"
                tokenId: BTC
                createdAt: "2026-03-04T12:00:00.000Z"
                data:
                  token: BTC
                  composite:
                    score: 72
                    signal: GO
                    confidence: 0.85
                  spot:
                    score: 42
                    decision: GO
                    bias: Pumping
                    confidence: 0.82
                    components:
                      - name: Volume Confluence
                        score: 8.2
                        band: strong_positive
                  perps:
                    score: 38
                    decision: GO
                    bias: Long
                    confidence: 0.88
                    squeezeRisk: High
                    components:
                      - name: Funding Pressure
                        score: -2.5
                        band: moderate_negative
                      - name: OI Momentum
                        score: 6.1
                        band: strong_positive
                  context:
                    funding: elevated
                    positioning: heavily_long
                    oiTrend: declining
                    basisSpread: normal
                    liquidationPressure: longs_exposed
                  market:
                    regime: Risk-On
                    btcDominance: 58.2
                    fearGreed: 72
                    altSeason: false
                  derivSymbols: [BTC, ETH, SOL, AVAX]
                  hasDerivatives: true
                  sources:
                    - provider: Coinglass
                      api: v4
                      url: "https://open-api-v4.coinglass.com"
                  meta:
                    timestamp: "2026-03-04T12:00:00.000Z"
                    schemaVersion: "2.0"
                    signalId: a3f8c9d1e2b04567
        "400":
          description: Missing requestId parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "410":
          description: |
            Snapshot expired — DynamoDB TTL removed it after 72 hours.
            Re-request fresh data via the data-push endpoint.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: stale_data
                  message:
                    type: string
                    example: "Data push snapshot expired after 72 hours. Re-request for fresh data."
                  requestId:
                    type: string

  # ── Bundles ───────────────────────────────────────────────────────────────
  /v1/api/bundles:
    get:
      tags: [Bundles]
      summary: List bundles
      description: |
        Returns all prepaid data-push bundles for the user, plus the bundle catalog.
      operationId: listBundles
      responses:
        "200":
          description: Bundle list and catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BundleListResponse"
              example:
                bundles:
                  - bundleId: "c3d4e5f6-a7b8-9012-cdef-345678901234"
                    userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
                    bundleSize: 350
                    remaining: 287
                    purchasedAt: "2026-02-20T10:30:00.000Z"
                    expiresAt: null
                catalog:
                  - name: "$5 bundle"
                    pushes: 60
                    price: 5
                  - name: "$25 bundle"
                    pushes: 350
                    price: 25
                  - name: "$50 bundle"
                    pushes: 800
                    price: 50
                perRequestPrice: 0.10
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      tags: [Bundles]
      summary: Purchase a bundle
      description: |
        Purchase a prepaid data-push bundle via x402 USDC payment.
        Pro and Business tiers only (apiAccess required).

        **Bundles**:
        - $5 bundle: 60 pushes ($0.08/push)
        - $25 bundle: 350 pushes ($0.07/push)
        - $50 bundle: 800 pushes ($0.06/push)
      operationId: purchaseBundle
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [bundleIndex, walletAddress]
              properties:
                bundleIndex:
                  type: integer
                  enum: [0, 1, 2]
                  description: "Bundle tier: 0=$5, 1=$25, 2=$50"
                walletAddress:
                  type: string
                  description: Wallet address to associate the bundle with
            example:
              bundleIndex: 1
              walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
      responses:
        "201":
          description: Bundle purchased
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Bundle"
              example:
                bundleId: "d4e5f6a7-b8c9-0123-def4-567890123456"
                userId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                walletAddress: "0xabcdef0123456789abcdef0123456789abcdef01"
                bundleName: "$25 bundle"
                totalPushes: 350
                remaining: 350
                price: 25
                purchasedAt: "2026-03-04T12:00:00.000Z"
                payer: "0xabcdef0123456789abcdef0123456789abcdef01"
                transactionId: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          description: Tier does not have API access
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Payment already processed (replay protection)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Agent API (x402-only, no Cognito) ──────────────────────────────────
  /v1/agent/score:
    get:
      tags: [Agent API]
      summary: Get single-asset derived scoring data (agent-native, x402 payment, business-tier)
      description: |
        Returns derived scoring data with categorical context for a single crypto asset — no raw provider metrics.
        Mirrors `/v1/api/data/{token}`. Designed for AI agents — no Cognito authentication required.
        Payment IS authentication via x402.

        **Data tier**: Always business-tier (all futures tokens included).

        **Returns**: Composite score (0-100, tanh-normalized), confidence (0-1), signal (GO/NO-GO/NEUTRAL),
        spot and perps sub-scores with scored components, categorical context (funding, positioning, OI trend,
        basis spread, liquidation pressure), market regime, `derivSymbols` (array of futures tokens covered),
        and metadata.

        **Data freshness**: Refreshes every 30s (business-tier cadence).

        **Pricing**: $0.10 USDC per request.

        **Abuse resistance**: Unpaid requests are rate-limited by source IP.
        5 unpaid requests in 60s → 24h ban, 50 in 300s → 48h ban.
        Successful payment immediately clears any ban.
      operationId: agentGetScore
      security: []
      x-x402:
        priceUsd: 0.10
        schemes: ["exact"]
        networks: ["eip155:8453", "eip155:43114", "eip155:137", "eip155:42161", "eip155:10", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"]
        asset: USDC
      parameters:
        - name: symbol
          in: query
          required: true
          description: "Asset ticker symbol (e.g. BTC, ETH, SOL)"
          schema:
            type: string
            pattern: "^[A-Z]{2,10}$"
          example: BTC
      responses:
        "200":
          description: Derived scoring data for the requested asset
          headers:
            PAYMENT-RESPONSE:
              description: Base64-encoded settlement result from x402 facilitator
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DerivedAssetResponse"
              example:
                token: BTC
                composite:
                  score: 72
                  signal: GO
                  confidence: 0.85
                spot:
                  score: 42
                  decision: GO
                  bias: Pumping
                  confidence: 0.82
                  components:
                    - name: Volume Confluence
                      score: 8.2
                      band: strong_positive
                    - name: Trend Alignment
                      score: 3.1
                      band: moderate_positive
                    - name: RSI Reversal
                      score: -1.4
                      band: weak_negative
                perps:
                  score: 38
                  decision: GO
                  bias: Long
                  confidence: 0.88
                  squeezeRisk: High
                  components:
                    - name: Funding Pressure
                      score: -2.5
                      band: moderate_negative
                    - name: OI Momentum
                      score: 6.1
                      band: strong_positive
                    - name: Liquidation Risk
                      score: 1.8
                      band: weak_positive
                    - name: Basis Arbitrage
                      score: 0.4
                      band: weak_positive
                context:
                  funding: elevated
                  positioning: heavily_long
                  oiTrend: declining
                  basisSpread: normal
                  liquidationPressure: longs_exposed
                market:
                  regime: Risk-On
                  btcDominance: 58.2
                  fearGreed: 72
                  altSeason: false
                derivSymbols: [BTC, ETH, SOL, AVAX, BNB, XRP, ADA, DOGE, LINK, DOT, UNI, NEAR, ARB, OP, SUI, APT, SEI, TIA, JUP, WIF, PEPE, BONK, FET, RENDER, TAO, INJ, PENDLE, ENA, STRK, ZRO, W, EIGEN, ETHFI, PYTH, JTO, WLD, MEME, 1000SATS, ORDI]
                hasDerivatives: true
                sources:
                  - provider: Coinglass
                    api: v4
                    url: "https://open-api-v4.coinglass.com"
                meta:
                  timestamp: "2026-03-03T12:00:00.000Z"
                  schemaVersion: "2.0"
                  signalId: a3f8c9d1e2b04567
        "400":
          $ref: "#/components/responses/BadRequest"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "404":
          description: Unknown asset symbol
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /v1/agent/data:
    get:
      tags: [Agent API]
      summary: Get multi-asset derived scoring data (agent-native, x402 payment, business-tier)
      description: |
        Returns derived scoring data with categorical context for all tracked assets — no raw provider metrics.
        Designed for AI agents — no Cognito authentication required. Payment IS authentication via x402.

        **Data tier**: Always business-tier (all futures tokens included).

        **Returns**: All assets with composite/spot/perps scores, scored components, categorical context
        (funding, positioning, OI trend, basis spread, liquidation pressure), market regime,
        `derivSymbols` (array of futures tokens covered per asset), asset ordering, and metadata.
        Optionally filter to specific symbols with `?symbols=BTC,ETH,SOL`.

        **Data freshness**: Refreshes every 30s (business-tier cadence).

        **Pricing**: $0.45 USDC per request.

        **Abuse resistance**: Same escalating ban model as `/v1/agent/score`.
      operationId: agentGetData
      security: []
      x-x402:
        priceUsd: 0.45
        schemes: ["exact"]
        networks: ["eip155:8453", "eip155:43114", "eip155:137", "eip155:42161", "eip155:10", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"]
        asset: USDC
      parameters:
        - name: symbols
          in: query
          required: false
          description: "Comma-separated list of symbols to filter (default: all)"
          schema:
            type: string
          example: "BTC,ETH,SOL"
      responses:
        "200":
          description: Derived scoring data for all tracked assets
          headers:
            PAYMENT-RESPONSE:
              description: Base64-encoded settlement result from x402 facilitator
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DerivedMultiAssetResponse"
              example:
                assets:
                  BTC:
                    token: BTC
                    composite:
                      score: 72
                      signal: GO
                      confidence: 0.85
                    spot:
                      score: 42
                      decision: GO
                      bias: Pumping
                      confidence: 0.82
                      components:
                        - name: Volume Confluence
                          score: 8.2
                          band: strong_positive
                        - name: Trend Alignment
                          score: 3.1
                          band: moderate_positive
                    perps:
                      score: 38
                      decision: GO
                      bias: Long
                      confidence: 0.88
                      squeezeRisk: High
                      components:
                        - name: Funding Pressure
                          score: -2.5
                          band: moderate_negative
                        - name: OI Momentum
                          score: 6.1
                          band: strong_positive
                    context:
                      funding: elevated
                      positioning: heavily_long
                      oiTrend: declining
                      basisSpread: normal
                      liquidationPressure: longs_exposed
                    derivSymbols: [BTC, ETH, SOL, AVAX, BNB, XRP, ADA, DOGE]
                    hasDerivatives: true
                  ETH:
                    token: ETH
                    composite:
                      score: 55
                      signal: NEUTRAL
                      confidence: 0.79
                    spot:
                      score: 28
                      decision: STARTER
                      bias: Chop
                      confidence: 0.74
                      components:
                        - name: Volume Confluence
                          score: 2.1
                          band: moderate_positive
                        - name: Trend Alignment
                          score: -0.8
                          band: weak_negative
                    perps:
                      score: 22
                      decision: STARTER
                      bias: Neutral
                      confidence: 0.83
                      squeezeRisk: Medium
                      components:
                        - name: Funding Pressure
                          score: 0.3
                          band: weak_positive
                        - name: OI Momentum
                          score: 1.9
                          band: weak_positive
                    context:
                      funding: neutral
                      positioning: balanced
                      oiTrend: stable
                      basisSpread: normal
                      liquidationPressure: balanced
                    derivSymbols: [BTC, ETH, SOL, AVAX, BNB, XRP, ADA, DOGE]
                    hasDerivatives: true
                  AVAX:
                    token: AVAX
                    composite:
                      score: 38
                      signal: NO-GO
                      confidence: 0.71
                    spot:
                      score: 18
                      decision: NO-GO
                      bias: Dumping
                      confidence: 0.68
                      components:
                        - name: Volume Confluence
                          score: -3.2
                          band: moderate_negative
                        - name: Trend Alignment
                          score: -4.1
                          band: moderate_negative
                    perps:
                      score: 15
                      decision: NO-GO
                      bias: Short
                      confidence: 0.73
                      squeezeRisk: Low
                      components:
                        - name: Funding Pressure
                          score: -1.1
                          band: weak_negative
                        - name: OI Momentum
                          score: -2.8
                          band: moderate_negative
                    context:
                      funding: negative
                      positioning: heavily_short
                      oiTrend: declining
                      basisSpread: backwardation
                      liquidationPressure: shorts_exposed
                    derivSymbols: [BTC, ETH, SOL, AVAX, BNB, XRP, ADA, DOGE]
                    hasDerivatives: true
                assetOrder: [BTC, ETH, AVAX]
                market:
                  regime: Risk-On
                  btcDominance: 58.2
                  fearGreed: 72
                  altSeason: false
                sources:
                  - provider: Coinglass
                    api: v4
                    url: "https://open-api-v4.coinglass.com"
                meta:
                  timestamp: "2026-03-03T12:00:00.000Z"
                  schemaVersion: "2.0"
                  signalId: b7e2d4f1a9c83061
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"

# ── Components ──────────────────────────────────────────────────────────────
components:
  securitySchemes:
    CognitoAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Cognito ID token (obtained via InitiateAuth with USER_PASSWORD_AUTH flow)

    x402Payment:
      type: apiKey
      in: header
      name: PAYMENT-SIGNATURE
      description: Base64-encoded x402 payment signature for USDC settlement

    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API key for programmatic access (Basic, Pro, Business tiers).
        Format: `ak_live_<32 hex chars>` (production) or `ak_test_<32 hex chars>` (testnet).
        Create keys via `POST /v1/user/api-keys` (requires Cognito JWT).

  responses:
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Unauthorized

    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

    Forbidden:
      description: Tier does not support this feature
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "Your plan does not support push notifications"

    TooManyRequests:
      description: |
        Abuse resistance — too many unpaid requests.
        Escalating bans: 5 unpaid/60s → 24h ban, 50 unpaid/300s → 48h ban.
        Successful x402 payment clears the ban immediately.
      headers:
        Retry-After:
          description: Seconds until the ban expires
          schema:
            type: integer
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Too many unpaid requests
              retryAfter:
                type: integer
              bannedUntil:
                type: string
                format: date-time

    PaymentRequired:
      description: x402 payment required
      headers:
        PAYMENT-REQUIRED:
          description: Base64-encoded JSON with payment requirements (x402 protocol)
          schema:
            type: string
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: Payment required
              message:
                type: string
                example: Include a PAYMENT-SIGNATURE header with your x402 payment to access this resource.

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Error message
      required: [error]

    UserProfile:
      type: object
      properties:
        userId:
          type: string
          description: Cognito user ID (sub claim)
        email:
          type: string
          format: email
        planTier:
          type: string
          enum: [free, basic, pro, business]
        role:
          type: string
          enum: [user, tester, moderator, admin]
          description: "User role (default: user). Admin-only field."
        effectiveTier:
          type: string
          enum: [free, basic, pro, business]
          description: "Resolved tier after applying overrides and redemptions (tierOverride > redemption > subscription > free)"
        tierOverride:
          type: string
          enum: [free, basic, pro, business]
          nullable: true
          description: "Tier override for testing (tester+ roles). Null when not active."
        arenaUsername:
          type: string
          nullable: true
        deliveryPreference:
          type: string
          enum: [email, push, webhook, both, all, "email,push", "email,webhook", "push,webhook", "email,push,webhook"]
          description: |
            Active notification channels. Legacy aliases: `both` = email+push, `all` = email+push+webhook.
            Comma-separated combos are preferred for new integrations.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    Catalog:
      type: object
      properties:
        tokens:
          type: array
          items:
            type: string
          description: Available token symbols
          example: [BTC, ETH, SOL, AVAX, BNB, XRP, ADA, DOGE]
        metrics:
          type: object
          additionalProperties:
            type: object
            properties:
              type:
                type: string
                enum: [number, enum]
              min:
                type: number
              max:
                type: number
              presets:
                type: array
                items:
                  type: object
              values:
                type: array
                items:
                  type: string
          description: |
            Available metrics with type info and presets. Decision metrics
            (spot_decision, perps_decision) support risk tolerance variants
            via __LOW and __HIGH suffixes (e.g. spot_decision__LOW). These
            compound keys are constructed client-side and validated server-side.
        tier:
          type: string
        maxSubscriptions:
          type: integer
        mode:
          type: string
          enum: [preset, custom, full]
          nullable: true

    Subscription:
      type: object
      properties:
        userId:
          type: string
        subId:
          type: string
          description: "Format: {tokenId}#{metricName}"
        tokenId:
          type: string
          example: BTC
        metricName:
          type: string
          description: "Primary metric name. May include __LOW or __HIGH suffix for risk-tolerance variants."
          example: spot_score
        label:
          type: string
          description: User-readable label for the subscription
          example: "BTC spot bullish"
        groups:
          type: array
          description: "Alert conditions in OR-of-ANDs format. Each inner array is an AND group; outer array is OR."
          items:
            type: array
            items:
              type: object
              properties:
                metricName:
                  type: string
                thresholdOp:
                  type: string
                  enum: [gt, lt, gte, lte, eq]
                thresholdVal:
                  oneOf:
                    - type: number
                    - type: string
        cooldownMinutes:
          type: integer
          description: "Alert cooldown in minutes (0 = realtime, max 1440 = 24h)"
          example: 5
        thresholdOp:
          type: string
          enum: [gt, lt, gte, lte, eq]
        thresholdVal:
          oneOf:
            - type: number
            - type: string
        bucketHash:
          type: string
          description: Config bucket hash (16-char hex)
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateSubscriptionRequest:
      type: object
      required: [tokenId, label]
      description: |
        Create a subscription. Provide conditions in one of three formats:
        1. `groups` — full OR-of-ANDs: `[[{metric,op,val},{metric,op,val}],[{metric,op,val}]]`
        2. `conditions` — single AND group: `[{metric,op,val},{metric,op,val}]`
        3. Flat fields — single condition: `metricName` + `thresholdOp` + `thresholdVal`
      properties:
        tokenId:
          type: string
          description: Token symbol
          example: BTC
        label:
          type: string
          description: User-readable label (required, max 100 chars)
          maxLength: 100
          example: "BTC spot bullish"
        groups:
          type: array
          description: "OR-of-ANDs condition groups. Each inner array is an AND group."
          items:
            type: array
            items:
              type: object
              required: [metricName, thresholdOp, thresholdVal]
              properties:
                metricName:
                  type: string
                  description: "Metric from catalog. Use __LOW/__HIGH suffix for risk-tolerance variants on decision metrics."
                thresholdOp:
                  type: string
                  enum: [gt, lt, gte, lte, eq]
                thresholdVal:
                  oneOf:
                    - type: number
                    - type: string
        metricName:
          type: string
          description: "Metric name (flat single-condition format). Supports __LOW/__HIGH suffixes."
          example: spot_score
        thresholdOp:
          type: string
          enum: [gt, lt, gte, lte, eq]
          description: Comparison operator (flat single-condition format)
        thresholdVal:
          oneOf:
            - type: number
            - type: string
          description: Threshold value (flat single-condition format)
          example: 5
        cooldownMinutes:
          type: integer
          minimum: 0
          maximum: 1440
          default: 5
          description: "Alert cooldown in minutes. 0 = realtime (no suppression). Min 1 (or 0). Default 5."

    SubscriptionPaymentResponse:
      type: object
      properties:
        tier:
          type: string
        billingCycle:
          type: string
        price:
          type: number
        currency:
          type: string
          example: USDC
        expiresAt:
          type: string
          format: date-time
        payer:
          type: string
          description: Wallet address that paid
        mode:
          type: string
          enum: [test, live]
        renewed:
          type: boolean
          description: Present and true when the subscription was renewed (extended from existing expiry) rather than newly created or upgraded
        message:
          type: string

    PaymentStatus:
      type: object
      properties:
        tier:
          type: string
          enum: [free, basic, pro, business]
        tierName:
          type: string
        billingCycle:
          type: string
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        maxSubs:
          type: integer
        apiAccess:
          type: boolean
        freeDataPushes:
          type: integer
        mode:
          type: string
          enum: [test, live]
        expired:
          type: boolean
          description: Present when subscription has expired
        graceDaysRemaining:
          type: integer
          description: Days remaining in 7-day grace period (0 if exhausted)
        downgraded:
          type: boolean
          description: Present when grace period has expired
        includedPushes:
          type: object
          description: Monthly included push balance (Pro+ only, absent for tiers with 0 included pushes)
          properties:
            total:
              type: integer
              description: Total included pushes per billing period (from TIERS.freeDataPushes)
            used:
              type: integer
              description: Pushes consumed in the current period
            remaining:
              type: integer
              description: Pushes remaining in the current period
            periodStart:
              type: string
              format: date-time
              description: Start of the current 30-day billing period
            periodEnd:
              type: string
              format: date-time
              description: End of the current billing period (resets after this date)

    RegisterWalletRequest:
      type: object
      required: [walletAddress, chain, signature, timestamp]
      properties:
        walletAddress:
          type: string
          description: "EVM: 0x-prefixed 40-hex. Solana: base58 32-44 chars."
          example: "0xabcdef0123456789abcdef0123456789abcdef01"
        chain:
          type: string
          enum: [base, avalanche, polygon, arbitrum, optimism, solana]
          description: Blockchain network
        signature:
          type: string
          description: "EVM: 0x-prefixed 130-hex (65 bytes). Solana: base58 64 bytes."
        timestamp:
          type: integer
          description: Unix timestamp (seconds) used in the challenge message

    WalletLink:
      type: object
      properties:
        userId:
          type: string
        walletAddress:
          type: string
        chain:
          type: string
          enum: [base, avalanche, polygon, arbitrum, optimism, solana]
        verifiedAt:
          type: string
          format: date-time

    DataPushResponse:
      type: object
      description: |
        Derived scoring data for a single token, returned by data-push endpoint.
        Top-level fields mirror DerivedAssetResponse, plus requestId and payment metadata.
      properties:
        token:
          type: string
          description: Asset ticker symbol
          example: BTC
        composite:
          $ref: "#/components/schemas/DerivedComposite"
        spot:
          $ref: "#/components/schemas/DerivedSubScore"
        perps:
          nullable: true
          description: Perpetual futures scoring data (null if no derivatives data for this asset)
          allOf:
            - $ref: "#/components/schemas/DerivedPerpsSubScore"
        context:
          $ref: "#/components/schemas/DerivedContext"
        market:
          $ref: "#/components/schemas/MarketRegime"
        derivSymbols:
          type: array
          items:
            type: string
          description: Futures tokens covered at the requesting tier
          example: ["BTC", "ETH", "SOL", "AVAX"]
        hasDerivatives:
          type: boolean
          description: Whether this asset has derivatives data
        sources:
          type: array
          items:
            $ref: "#/components/schemas/DataSource"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
        requestId:
          type: string
          description: |
            Unique snapshot ID for 72-hour re-retrieval via `GET /v1/data/push/{requestId}`.
            Format: ISO-timestamp-UUID-prefix.
          example: "2026-02-24T12-30-00-000Z-a1b2c3d4"
        mode:
          type: string
          enum: [test, live]
        bundle:
          type: object
          description: Present when bundle deduction was used
          properties:
            bundleId:
              type: string
            remaining:
              type: integer
        includedPush:
          type: object
          description: Present when an included monthly push was consumed
          properties:
            used:
              type: integer
              description: Pushes consumed so far this period (including this one)
            total:
              type: integer
              description: Total included pushes per period
            remaining:
              type: integer
              description: Pushes remaining after this deduction

    DataPushSnapshot:
      type: object
      description: |
        A 72-hour snapshot of derived scoring data. Created by data-push payments (x402/bundle)
        or by the alert trigger when an alert fires/clears. Webhook `data.url` points here.
      properties:
        requestId:
          type: string
          description: "Unique snapshot ID (format: ISO-timestamp-UUID-prefix)"
          example: "2026-03-04T12-00-00-000Z-a1b2c3d4"
        tokenId:
          type: string
          description: Token symbol the snapshot is for
          example: BTC
        createdAt:
          type: string
          format: date-time
          description: When the snapshot was created (ISO 8601)
        data:
          description: Full derived scoring data (composite score, spot/perps sub-scores, context, components)
          $ref: "#/components/schemas/DerivedAssetResponse"

    MessagingLink:
      type: object
      description: A linked messaging platform account.
      properties:
        userId:
          type: string
          description: AttaGo user ID
        platform:
          type: string
          enum: [telegram]
          description: Messaging platform identifier
        chatId:
          type: string
          description: Platform-specific chat/DM ID
        username:
          type: string
          description: Display name on the platform
        linkedAt:
          type: string
          format: date-time
          description: When the account was linked

    WebhookDeliveryPayload:
      type: object
      description: |
        Webhook v2 delivery payload — slim callback design. Sent as HTTP POST body to registered webhook URLs.
        Signed with HMAC-SHA256 (secret from webhook registration). Consumers fetch full data via `data.url`.
      required: [event, version, environment, alert, data, timestamp]
      properties:
        event:
          type: string
          enum: [alert]
          description: Event type — always `"alert"`
        version:
          type: string
          enum: ["2"]
          description: Payload schema version
        environment:
          type: string
          enum: [production, staging, dev]
          description: Deployment environment that generated this alert
        alert:
          type: object
          required: [id, label, token, state]
          properties:
            id:
              type: string
              description: "Subscription ID (format: `{tokenId}#{metricName}`)"
              example: "BTC#spot_score"
            label:
              type: string
              description: "User's custom alert label, or `\"{token} alert\"` if none set"
              example: "BTC spot bullish"
            token:
              type: string
              description: Token symbol
              example: BTC
            state:
              type: string
              enum: [triggered, resolved]
              description: Whether the alert condition was met (triggered) or no longer met (resolved)
        data:
          type: object
          required: [url, expiresAt]
          description: |
            Callback data for fetching full scoring details.
            `url` is null if the alert-time snapshot failed — use `fallbackUrl` instead.
          properties:
            url:
              type: string
              format: uri
              nullable: true
              description: |
                Stable callback URL to `GET /v1/data/push/{requestId}` — returns the exact
                derived scoring data that was live when the alert fired. No auth required.
                Available for 72 hours. Null if snapshot creation failed.
              example: "https://api.attago.bid/v1/data/push/2026-03-04T12-00-00-000Z-a1b2c3d4"
            expiresAt:
              type: string
              format: date-time
              description: ISO 8601 timestamp — when the snapshot expires (~72h from alert time)
              example: "2026-03-07T12:00:00.000Z"
            fallbackUrl:
              type: string
              format: uri
              description: "Only present when `url` is null. Points to latest public data as a fallback."
              example: "https://attago.bid/data/latest.json"
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when the alert fired
          example: "2026-03-04T12:00:00.000Z"
      example:
        event: alert
        version: "2"
        environment: production
        alert:
          id: "BTC#spot_score"
          label: "BTC spot bullish"
          token: BTC
          state: triggered
        data:
          url: "https://api.attago.bid/v1/data/push/2026-03-04T12-00-00-000Z-a1b2c3d4"
          expiresAt: "2026-03-07T12:00:00.000Z"
        timestamp: "2026-03-04T12:00:00.000Z"

    WebhookDeliveryPayloadResolved:
      description: "Example of a resolved alert webhook payload (data.url is null when snapshot failed)"
      allOf:
        - $ref: "#/components/schemas/WebhookDeliveryPayload"
      example:
        event: alert
        version: "2"
        environment: production
        alert:
          id: "ETH#perps_score"
          label: "ETH perps bearish"
          token: ETH
          state: resolved
        data:
          url: null
          expiresAt: "2026-03-07T14:30:00.000Z"
          fallbackUrl: "https://attago.bid/data/latest.json"
        timestamp: "2026-03-04T14:30:00.000Z"

    Bundle:
      type: object
      properties:
        bundleId:
          type: string
          format: uuid
        userId:
          type: string
        walletAddress:
          type: string
        bundleName:
          type: string
          example: "$5 bundle"
        totalPushes:
          type: integer
          example: 30
        remaining:
          type: integer
        price:
          type: number
          example: 5
        purchasedAt:
          type: string
          format: date-time
        payer:
          type: string
        transactionId:
          type: string

    BundleListResponse:
      type: object
      properties:
        bundles:
          type: array
          items:
            $ref: "#/components/schemas/Bundle"
        catalog:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              price:
                type: number
              pushes:
                type: integer
              perPush:
                type: number
        perRequestPrice:
          type: number
          example: 0.10

    # ── Derived Data Schemas ──────────────────────────────────────────────

    DerivedComponent:
      type: object
      description: A single scored component within spot or perps analysis
      properties:
        name:
          type: string
          description: Component name (e.g. "Volume Confluence", "Funding Pressure")
          example: Volume Confluence
        score:
          type: number
          description: Component score (sign indicates direction)
          example: 8.2
        band:
          type: string
          description: Categorical band for this component
          enum:
            - strong_negative
            - moderate_negative
            - weak_negative
            - neutral
            - weak_positive
            - moderate_positive
            - strong_positive
          example: strong_positive
      required: [name, score, band]

    DerivedComposite:
      type: object
      description: Composite score combining spot and perps analysis
      properties:
        score:
          type: integer
          description: "Composite score (0-100, tanh-normalized)"
          minimum: 0
          maximum: 100
          example: 72
        signal:
          type: string
          description: Trading signal derived from composite score
          enum: [GO, NO-GO, NEUTRAL]
          example: GO
        confidence:
          type: number
          description: "Confidence level (0-1)"
          minimum: 0
          maximum: 1
          example: 0.85
      required: [score, signal, confidence]

    DerivedSubScore:
      type: object
      description: Spot market scoring data with scored components
      properties:
        score:
          type: number
          description: Sub-score value
          example: 42
        decision:
          type: string
          description: Trading decision for this sub-score
          enum: [GO, STARTER, NO-GO]
          example: GO
        bias:
          type: string
          description: "Spot market bias (Pumping = sell signal, Dumping = buy-the-dip, Chop = sideways)"
          enum: [Pumping, Dumping, Chop]
          example: Pumping
        confidence:
          type: number
          description: "Confidence level (0-1)"
          minimum: 0
          maximum: 1
          example: 0.82
        components:
          type: array
          description: Scored components contributing to this sub-score
          items:
            $ref: "#/components/schemas/DerivedComponent"
      required: [score, decision, bias, confidence, components]

    DerivedPerpsSubScore:
      type: object
      description: Perpetual futures scoring data with scored components and squeeze risk
      properties:
        score:
          type: number
          description: Sub-score value
          example: 38
        decision:
          type: string
          description: Trading decision for this sub-score
          enum: [GO, STARTER, NO-GO]
          example: GO
        bias:
          type: string
          description: "Perpetuals market bias (Long = bullish positioning, Short = bearish, Neutral = no direction)"
          enum: [Long, Short, Neutral]
          example: Long
        confidence:
          type: number
          description: "Confidence level (0-1)"
          minimum: 0
          maximum: 1
          example: 0.88
        squeezeRisk:
          type: string
          description: Short/long squeeze risk level
          enum: [Low, Medium, High]
          example: High
        components:
          type: array
          description: Scored components contributing to this sub-score
          items:
            $ref: "#/components/schemas/DerivedComponent"
      required: [score, decision, bias, confidence, squeezeRisk, components]

    DerivedContext:
      type: object
      description: Categorical context derived from market data (all fields nullable for spot-only assets)
      properties:
        funding:
          type: string
          nullable: true
          description: Funding rate regime
          enum: [negative, neutral, elevated, extreme]
          example: elevated
        positioning:
          type: string
          nullable: true
          description: Market positioning sentiment
          enum: [heavily_short, balanced, moderately_long, heavily_long]
          example: heavily_long
        oiTrend:
          type: string
          nullable: true
          description: Open interest trend
          enum: [declining, stable, increasing, aggressive_increase]
          example: declining
        basisSpread:
          type: string
          nullable: true
          description: Futures-spot basis spread regime
          enum: [backwardation, normal, elevated, extreme_contango]
          example: normal
        liquidationPressure:
          type: string
          nullable: true
          description: Liquidation pressure direction
          enum: [shorts_exposed, balanced, longs_exposed]
          example: longs_exposed

    MarketRegime:
      type: object
      description: Broad market regime indicators
      properties:
        regime:
          type: string
          description: Current market regime
          enum: [Risk-On, Risk-Off, Chop]
          example: Risk-On
        btcDominance:
          type: number
          nullable: true
          description: "Bitcoin dominance percentage"
          example: 58.2
        fearGreed:
          type: integer
          nullable: true
          description: "Fear & Greed index value (0-100)"
          minimum: 0
          maximum: 100
          example: 72
        altSeason:
          type: boolean
          description: Whether altcoin season is active
          example: false
      required: [regime, btcDominance, fearGreed, altSeason]

    DataSource:
      type: object
      description: Data source attribution
      properties:
        provider:
          type: string
          example: Coinglass
        api:
          type: string
          example: v4
        url:
          type: string
          format: uri
          example: "https://open-api-v4.coinglass.com"
      required: [provider, api, url]

    ResponseMeta:
      type: object
      description: Response metadata
      properties:
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when the data was generated
          example: "2026-03-02T12:00:00.000Z"
        schemaVersion:
          type: string
          description: Schema version for this response shape
          example: "2.0"
        signalId:
          type: string
          description: "Unique signal identifier (16-char hex, sha256 of token+ts+version)"
          pattern: "^[0-9a-f]{16}$"
          example: a3f8c9d1e2b04567
      required: [timestamp, schemaVersion, signalId]

    DerivedAssetResponse:
      type: object
      description: |
        Derived scoring data for a single asset — no raw provider metrics.
        Composite score is normalized (0-100) and not strictly additive of sub-scores.
      properties:
        token:
          type: string
          description: Asset ticker symbol
          example: BTC
        composite:
          $ref: "#/components/schemas/DerivedComposite"
        spot:
          $ref: "#/components/schemas/DerivedSubScore"
        perps:
          nullable: true
          description: Perpetual futures scoring data (null if no derivatives data for this asset)
          allOf:
            - $ref: "#/components/schemas/DerivedPerpsSubScore"
        context:
          $ref: "#/components/schemas/DerivedContext"
        market:
          $ref: "#/components/schemas/MarketRegime"
        derivSymbols:
          type: array
          items:
            type: string
          description: "Futures tokens covered at the requesting tier (x402 = all tokens)"
          example: ["BTC", "ETH", "SOL", "AVAX"]
        hasDerivatives:
          type: boolean
          description: Whether this asset has derivatives data
        sources:
          type: array
          items:
            $ref: "#/components/schemas/DataSource"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [token, composite, spot, context, market, derivSymbols, hasDerivatives, sources, meta]

    DerivedAssetEntry:
      type: object
      description: |
        Per-asset entry in a multi-asset response. Same as DerivedAssetResponse
        but without market, sources, and meta (hoisted to top level).
      properties:
        token:
          type: string
          description: Asset ticker symbol
          example: BTC
        composite:
          $ref: "#/components/schemas/DerivedComposite"
        spot:
          $ref: "#/components/schemas/DerivedSubScore"
        perps:
          nullable: true
          description: Perpetual futures scoring data (null if no derivatives data for this asset)
          allOf:
            - $ref: "#/components/schemas/DerivedPerpsSubScore"
        context:
          $ref: "#/components/schemas/DerivedContext"
        derivSymbols:
          type: array
          items:
            type: string
          description: "Futures tokens covered at the requesting tier (x402 = all tokens)"
          example: ["BTC", "ETH", "SOL", "AVAX"]
        hasDerivatives:
          type: boolean
          description: Whether this asset has derivatives data
      required: [token, composite, spot, context, derivSymbols, hasDerivatives]

    DerivedMultiAssetResponse:
      type: object
      description: |
        Derived scoring data for all tracked assets. Per-asset objects omit market, sources,
        and meta which are hoisted to the top level.
      properties:
        assets:
          type: object
          description: "Asset data keyed by symbol. Each value is a DerivedAssetEntry."
          additionalProperties:
            $ref: "#/components/schemas/DerivedAssetEntry"
        assetOrder:
          type: array
          items:
            type: string
          description: Ordered list of asset symbols (by composite score descending)
          example: ["BTC", "ETH", "SOL"]
        market:
          $ref: "#/components/schemas/MarketRegime"
        sources:
          type: array
          items:
            $ref: "#/components/schemas/DataSource"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [assets, assetOrder, market, sources, meta]
