openapi: 3.0.3
info:
  title: CoinOperated API
  version: 1.0.0
  description: |
    Self-custodial dollar-cost averaging (DCA) API for [coinoperated.finance](https://coinoperated.finance).

    **Browser auth:** `GET /api/auth/siwe/nonce` → sign SIWE → `POST /api/auth/siwe/verify`
    (sets HttpOnly `sessionToken` cookie).

    **Agent auth:** same nonce + SIWE → `POST /api/auth/siwe/verify/token` → JWT.
    Call authenticated routes with `Authorization: Bearer <accessToken>`.
    Additional signer addresses may SIWE as the parent user after the owner registers them.

    Successful responses are the unwrapped JSON value (no `{ "result": … }` wrapper).
    Errors: `{ "error": string }` (optional `details` on 500).

    Human docs: `/docs/api`. This file: `/openapi.yaml`.
  contact:
    name: CoinOperated
    url: https://coinoperated.finance

servers:
  - url: https://coinoperated.finance
    description: Production
  - url: http://localhost:3200
    description: Local development

tags:
  - name: Auth
    description: SIWE nonce, session cookie, JWT access tokens, logout
  - name: User
    description: Session probe, profile, balance, country, contact info
  - name: AdditionalSigners
    description: Extra EVM addresses allowed to SIWE as the parent user
  - name: AgentDelegations
    description: Stored EIP-7715 grants to additional signers (agents)
  - name: AgentActivity
    description: Relayer spends attributed to additional signers via memo webhooks
  - name: Plans
    description: Recurring DCA plans
  - name: TriggeredPlans
    description: Price-rule triggered purchase plans
  - name: Transfers
    description: Individual DCA / triggered execution attempts
  - name: Prices
    description: OHLC buckets and spot prices
  - name: Catalog
    description: Chains, tokens, DCA plan types
  - name: Calculator
    description: Historical DCA simulator (public)
  - name: Relayer
    description: Relayer / create-plan capability hints
  - name: Webhooks
    description: Inbound callbacks (relayer signature / Telegram secret)
  - name: Metrics
    description: Anonymous product metrics
  - name: Health
    description: Liveness

security: []

paths:
  /api/healthz:
    get:
      tags: [Health]
      summary: Health check
      operationId: getHealthz
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    example: true

  /api/auth/siwe/nonce:
    get:
      tags: [Auth]
      summary: Create SIWE nonce
      operationId: getSiweNonce
      responses:
        "200":
          description: Nonce and SIWE domain/chain for the client message
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiweNonceResponse"
        "400":
          $ref: "#/components/responses/Error"

  /api/auth/siwe/verify:
    post:
      tags: [Auth]
      summary: Verify SIWE and set session cookie
      description: |
        On success, sets HttpOnly Secure cookie `sessionToken`.
        Response body is `{ user }` only (`__sessionToken` is never returned in JSON).
        Unknown addresses upsert a new user. Registered additional signers resolve to the parent user.
      operationId: verifySiweCookie
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SiweVerifyRequest"
      responses:
        "200":
          description: Authenticated; cookie set
          headers:
            Set-Cookie:
              schema:
                type: string
              description: "sessionToken=...; HttpOnly; Secure; SameSite=Lax; Path=/"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiweVerifyResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/auth/siwe/verify/token:
    post:
      tags: [Auth]
      summary: Verify SIWE and issue JWT access token
      description: |
        For agents. Address must already be a primary wallet or registered additional signer
        (no new-user upsert). Returns a Bearer JWT (`sub` = parent UserId, `signer` = signing address).
        Soft-deleting an additional signer revokes outstanding JWTs for that signer.
      operationId: verifySiweToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SiweVerifyRequest"
      responses:
        "200":
          description: Access token issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiweTokenResponse"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/auth/logout:
    post:
      tags: [Auth]
      summary: Logout (revoke Redis session, clear cookie)
      operationId: logout
      security:
        - SessionCookie: []
        - BearerAuth: []
      responses:
        "200":
          description: Logged out (JWT callers get success; only cookie sessions are revoked server-side)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Error"

  /api/user:
    get:
      tags: [User]
      summary: Session status probe
      description: >-
        Returns session true with user when authenticated, otherwise session false.
      operationId: getUserSession
      security:
        - SessionCookie: []
        - BearerAuth: []
        - {}
      responses:
        "200":
          description: Session status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionStatusResponse"
    patch:
      tags: [User]
      summary: Update user timezone
      operationId: patchUser
      security:
        - SessionCookie: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [timezone]
              properties:
                timezone:
                  type: string
                  description: IANA timezone
                  example: America/Los_Angeles
      responses:
        "200":
          description: Updated user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/user/balance:
    get:
      tags: [User]
      summary: Get USDC balance (atoms)
      operationId: getUserBalance
      security:
        - SessionCookie: []
        - BearerAuth: []
      responses:
        "200":
          description: Balance in USDC base units (6 decimals)
          content:
            application/json:
              schema:
                type: object
                required: [balance]
                properties:
                  balance:
                    $ref: "#/components/schemas/USDCAmount"
        "401":
          $ref: "#/components/responses/Error"

  /api/user/country:
    get:
      tags: [User]
      summary: Infer country from edge headers / IP
      operationId: getUserCountry
      responses:
        "200":
          description: ISO-3166-1 alpha-2 country or null
          content:
            application/json:
              schema:
                type: object
                required: [country]
                properties:
                  country:
                    type: string
                    nullable: true
                    example: US
                  error:
                    type: string

  /api/user/contact:
    get:
      tags: [User]
      summary: List contact info
      operationId: listContacts
      security:
        - SessionCookie: []
        - BearerAuth: []
      responses:
        "200":
          description: Contact rows
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/UserContactInfo"
        "401":
          $ref: "#/components/responses/Error"
    post:
      tags: [User]
      summary: Add email or Telegram contact
      description: Provide exactly one of `emailAddress` or `telegramHandle`.
      operationId: addContact
      security:
        - SessionCookie: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddContactRequest"
      responses:
        "200":
          description: Created contact
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserContactInfo"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/user/contact/{contactInfoId}:
    delete:
      tags: [User]
      summary: Soft-delete contact info
      operationId: deleteContact
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/contactInfoId"
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/user/contact/verify-email:
    post:
      tags: [User]
      summary: Verify email with OTP
      operationId: verifyEmailContact
      security:
        - SessionCookie: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  pattern: "^[0-9]{6}$"
                  description: 6-digit OTP from email
      responses:
        "200":
          description: Verified
          content:
            application/json:
              schema:
                type: object
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/user/contact/resend-email-verification:
    post:
      tags: [User]
      summary: Resend email verification OTP
      operationId: resendEmailVerification
      security:
        - SessionCookie: []
        - BearerAuth: []
      responses:
        "200":
          description: Sent (rate-limited)
          content:
            application/json:
              schema:
                type: object
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/user/agent-activity:
    get:
      tags:
        - AgentActivity
      summary: List agent activity (relayer spends attributed via memo)
      operationId: listAgentActivity
      security:
        - SessionCookie: []
        - BearerAuth: []
      responses:
        "200":
          description: Activity rows newest first
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AgentActivity"
        "401":
          $ref: "#/components/responses/Error"

  /api/user/additional-signers:
    get:
      tags: [AdditionalSigners]
      summary: List additional signers
      operationId: listAdditionalSigners
      security:
        - SessionCookie: []
        - BearerAuth: []
      responses:
        "200":
          description: Signers
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdditionalSigner"
        "401":
          $ref: "#/components/responses/Error"
    post:
      tags: [AdditionalSigners]
      summary: Add additional signer address
      operationId: addAdditionalSigner
      security:
        - SessionCookie: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accountAddress]
              properties:
                accountAddress:
                  $ref: "#/components/schemas/EVMAccountAddress"
                label:
                  type: string
                  nullable: true
                  maxLength: 64
      responses:
        "200":
          description: Created or undeleted signer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdditionalSigner"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/user/additional-signers/{signerId}:
    delete:
      tags: [AdditionalSigners]
      summary: Soft-delete additional signer (revokes JWTs for that signer)
      operationId: removeAdditionalSigner
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/signerId"
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/user/additional-signers/{signerId}/delegations:
    get:
      tags: [AgentDelegations]
      summary: List stored agent delegations for an additional signer
      operationId: listAgentDelegationsForSigner
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/signerId"
      responses:
        "200":
          description: Stored delegations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AgentDelegation"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    post:
      tags: [AgentDelegations]
      summary: Store a newly signed agent delegation
      operationId: createAgentDelegation
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/signerId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [periodAmount, period, delegation, permissionContext]
              properties:
                periodAmount:
                  type: integer
                  description: USDC amount in atoms (6 decimals)
                period:
                  $ref: "#/components/schemas/EDcaPlanPeriod"
                delegation:
                  type: string
                  description: Serialized MetaMask Delegation JSON
                permissionContext:
                  $ref: "#/components/schemas/HexString"
                justification:
                  type: string
                  nullable: true
                  maxLength: 128
      responses:
        "200":
          description: Stored row
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentDelegation"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/user/additional-signers/{signerId}/delegations/{delegationId}/cancel:
    get:
      tags: [AgentDelegations]
      summary: Permission contexts for requestCancelDelegations
      operationId: getAgentDelegationCancelData
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/signerId"
        - $ref: "#/components/parameters/delegationId"
      responses:
        "200":
          description: Cancel items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PlanCancelItem"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/user/additional-signers/{signerId}/delegations/{delegationId}:
    delete:
      tags: [AgentDelegations]
      summary: Soft-delete stored agent delegation after wallet cancel
      operationId: deleteAgentDelegation
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/signerId"
        - $ref: "#/components/parameters/delegationId"
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/agent/delegations:
    get:
      tags: [AgentDelegations]
      summary: List stored delegations for the Bearer JWT signer
      description: |
        Bearer access token required (cookie sessions are rejected).
        Returns delegations whose `delegateAccountAddress` matches the JWT `signer` claim.
      operationId: listAgentDelegationsForBearer
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Stored delegations for this signer
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AgentDelegation"
        "401":
          $ref: "#/components/responses/Error"

  /api/plans:
    get:
      tags: [Plans]
      summary: List DCA plans
      operationId: listPlans
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/pageSize"
      responses:
        "200":
          description: Paged plans plus create-plan config
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListPlansResponse"
        "401":
          $ref: "#/components/responses/Error"
    post:
      tags: [Plans]
      summary: Create DCA plan
      operationId: createPlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePlanRequest"
      responses:
        "200":
          description: Created plan
          content:
            application/json:
              schema:
                type: object
                required: [plan]
                properties:
                  plan:
                    $ref: "#/components/schemas/DcaPlan"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/plans/{planId}:
    get:
      tags: [Plans]
      summary: Get a recurring or triggered plan and its holdings stats
      operationId: getPlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/planId"
      responses:
        "200":
          description: Plan detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlanDetailResponse"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      tags: [Plans]
      summary: Soft-delete DCA plan
      operationId: deletePlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/planId"
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/plans/{planId}/pause:
    post:
      tags: [Plans]
      summary: Pause DCA plan
      operationId: pausePlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/planId"
      responses:
        "200":
          description: Paused plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DcaPlan"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/plans/{planId}/resume:
    post:
      tags: [Plans]
      summary: Resume DCA plan
      operationId: resumePlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/planId"
      responses:
        "200":
          description: Resumed plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DcaPlan"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/plans/{planId}/cancel:
    get:
      tags: [Plans]
      summary: Cancel preview (permission contexts for wallet revoke)
      description: Read-only. Returns EIP-7715-style permission contexts for `requestCancelDelegations`.
      operationId: getPlanCancelContexts
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/planId"
      responses:
        "200":
          description: Per-chain cancel contexts
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PlanCancelItem"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/plans/{planId}/transfers:
    get:
      tags: [Plans]
      summary: List transfers for a DCA plan
      operationId: listPlanTransfers
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/planId"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/pageSize"
        - $ref: "#/components/parameters/fromTimestamp"
        - $ref: "#/components/parameters/toTimestamp"
      responses:
        "200":
          description: Paged transfers
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PagedTransfers"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/triggered-plans:
    get:
      tags: [TriggeredPlans]
      summary: List triggered plans
      operationId: listTriggeredPlans
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/pageSize"
      responses:
        "200":
          description: Paged triggered plans plus create config
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListTriggeredPlansResponse"
        "401":
          $ref: "#/components/responses/Error"
    post:
      tags: [TriggeredPlans]
      summary: Create triggered plan
      operationId: createTriggeredPlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTriggeredPlanRequest"
      responses:
        "200":
          description: Created plan
          content:
            application/json:
              schema:
                type: object
                required: [plan]
                properties:
                  plan:
                    $ref: "#/components/schemas/TriggeredPlan"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/triggered-plans/{planId}:
    get:
      tags: [TriggeredPlans]
      summary: Get triggered plan detail
      operationId: getTriggeredPlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/triggeredPlanId"
      responses:
        "200":
          description: Plan detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TriggeredPlanDetailResponse"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
    delete:
      tags: [TriggeredPlans]
      summary: Soft-delete triggered plan
      operationId: deleteTriggeredPlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/triggeredPlanId"
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/triggered-plans/{planId}/pause:
    post:
      tags: [TriggeredPlans]
      summary: Pause triggered plan
      operationId: pauseTriggeredPlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/triggeredPlanId"
      responses:
        "200":
          description: Paused
          content:
            application/json:
              schema:
                type: object
                required: [plan]
                properties:
                  plan:
                    $ref: "#/components/schemas/TriggeredPlan"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/triggered-plans/{planId}/resume:
    post:
      tags: [TriggeredPlans]
      summary: Resume triggered plan
      operationId: resumeTriggeredPlan
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/triggeredPlanId"
      responses:
        "200":
          description: Resumed
          content:
            application/json:
              schema:
                type: object
                required: [plan]
                properties:
                  plan:
                    $ref: "#/components/schemas/TriggeredPlan"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/triggered-plans/{planId}/cancel:
    get:
      tags: [TriggeredPlans]
      summary: Cancel preview for triggered plan
      operationId: getTriggeredPlanCancelContexts
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/triggeredPlanId"
      responses:
        "200":
          description: Per-chain cancel contexts
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PlanCancelItem"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/triggered-plans/{planId}/transfers:
    get:
      tags: [TriggeredPlans]
      summary: List transfers for a triggered plan
      operationId: listTriggeredPlanTransfers
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/triggeredPlanId"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/pageSize"
        - $ref: "#/components/parameters/fromTimestamp"
        - $ref: "#/components/parameters/toTimestamp"
      responses:
        "200":
          description: Paged transfers
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PagedTransfers"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/transfers/{transferId}:
    get:
      tags: [Transfers]
      summary: Get transfer detail
      operationId: getTransfer
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - name: transferId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Transfer with optional price entry
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DcaTransfer"
        "401":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"

  /api/prices:
    get:
      tags: [Prices]
      summary: OHLC price buckets
      operationId: getPrices
      security:
        - SessionCookie: []
        - BearerAuth: []
      parameters:
        - name: asset
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/EPriceAsset"
        - name: range
          in: query
          required: true
          schema:
            type: string
            description: Chart range key (e.g. 1d, 1w, 1m, 1y)
        - name: resolution
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: OHLC buckets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PriceBucket"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/prices/spot:
    get:
      tags: [Prices]
      summary: Latest spot prices (USD cents)
      operationId: getSpotPrices
      security:
        - SessionCookie: []
        - BearerAuth: []
      responses:
        "200":
          description: Spot prices
          content:
            application/json:
              schema:
                type: object
                properties:
                  ethPriceCents:
                    type: integer
                    nullable: true
                  btcPriceCents:
                    type: integer
                    nullable: true
                  paxgPriceCents:
                    type: integer
                    nullable: true
        "401":
          $ref: "#/components/responses/Error"

  /api/chains:
    get:
      tags: [Catalog]
      summary: List supported chains
      operationId: listChains
      responses:
        "200":
          description: Chain catalog
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ChainInfo"

  /api/tokens:
    get:
      tags: [Catalog]
      summary: List tokens
      operationId: listTokens
      parameters:
        - name: chainId
          in: query
          required: false
          schema:
            type: string
          description: Filter by ChainInfo id
      responses:
        "200":
          description: Token catalog
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TokenInfo"

  /api/dca-plan-types:
    get:
      tags: [Catalog]
      summary: List DCA plan type options
      operationId: listDcaPlanTypes
      responses:
        "200":
          description: Purchase destination catalog
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/DcaPlanType"

  /api/relayer/capabilities:
    get:
      tags: [Relayer]
      summary: Relayer / create-plan capability hints
      operationId: getRelayerCapabilities
      parameters:
        - name: chainId
          in: query
          required: false
          schema:
            type: string
          description: Decimal chain id (defaults to Arc)
      responses:
        "200":
          description: Addresses and fee atoms for plan creation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RelayerCapabilities"

  /api/dca-calculator/simulate:
    post:
      tags: [Calculator]
      summary: Simulate historical DCA
      operationId: simulateDca
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DcaSimulateRequest"
      responses:
        "200":
          description: Simulator result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DcaSimulateResponse"
        "400":
          $ref: "#/components/responses/Error"

  /api/webhooks/relayer:
    post:
      tags: [Webhooks]
      summary: Relayer status webhook
      description: |
        Inbound from 1Shot public relayer. Authenticated via Ed25519 signature
        over the payload (not SIWE). See public-relayer skill / JWKS.
      operationId: postRelayerWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RelayerWebhookBody"
      responses:
        "200":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"

  /api/webhooks/telegram:
    post:
      tags: [Webhooks]
      summary: Telegram bot webhook
      description: Requires header `X-Telegram-Bot-Api-Secret-Token`.
      operationId: postTelegramWebhook
      parameters:
        - name: X-Telegram-Bot-Api-Secret-Token
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [update_id]
              properties:
                update_id:
                  type: integer
                message:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Accepted
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Error"

  /api/metrics/browser:
    post:
      tags: [Metrics]
      summary: Record browser / OS metrics
      operationId: postBrowserMetrics
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [browserName, browserVersion, osName]
              properties:
                browserName:
                  type: string
                  minLength: 1
                  maxLength: 100
                browserVersion:
                  type: string
                  minLength: 1
                  maxLength: 50
                osName:
                  type: string
                  minLength: 1
                  maxLength: 100
                osVersion:
                  type: string
                  maxLength: 100
                osVersionName:
                  type: string
                  maxLength: 100
                platformType:
                  type: string
                  maxLength: 50
                engineName:
                  type: string
                  maxLength: 100
                engineVersion:
                  type: string
                  maxLength: 50
      responses:
        "200":
          description: Stored metrics row
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        "400":
          $ref: "#/components/responses/Error"

  /api/metrics/countries:
    get:
      tags: [Metrics]
      summary: Supported countries list
      operationId: getMetricsCountries
      responses:
        "200":
          description: Country codes
          content:
            application/json:
              schema:
                type: object
                required: [countries]
                properties:
                  countries:
                    type: array
                    items:
                      type: string

components:
  securitySchemes:
    SessionCookie:
      type: apiKey
      in: cookie
      name: sessionToken
      description: HttpOnly session from POST /api/auth/siwe/verify
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Access token from POST /api/auth/siwe/verify/token

  parameters:
    page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    pageSize:
      name: pageSize
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    fromTimestamp:
      name: from
      in: query
      schema:
        type: integer
      description: Unix timestamp (seconds), inclusive lower bound
    toTimestamp:
      name: to
      in: query
      schema:
        type: integer
      description: Unix timestamp (seconds), inclusive upper bound
    planId:
      name: planId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    triggeredPlanId:
      name: planId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Triggered plan id
    contactInfoId:
      name: contactInfoId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    signerId:
      name: signerId
      in: path
      required: true
      schema:
        type: string
        format: uuid

    delegationId:
      name: delegationId
      in: path
      required: true
      schema:
        type: string
        format: uuid

  responses:
    Error:
      description: Error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"

  schemas:
    ErrorBody:
      type: object
      required: [error]
      properties:
        error:
          type: string
        details:
          type: string

    SuccessResponse:
      type: object
      required: [success]
      properties:
        success:
          type: boolean

    EVMAccountAddress:
      type: string
      pattern: "^0x[a-fA-F0-9]{40}$"
      example: "0x0000000000000000000000000000000000000001"

    EVMContractAddress:
      type: string
      pattern: "^0x[a-fA-F0-9]{40}$"

    EVMChainId:
      type: string
      description: EIP-155 chain id as hex string
      example: "0x13b2"

    HexString:
      type: string
      pattern: "^0x[a-fA-F0-9]*$"

    USDCAmount:
      type: integer
      description: USDC atoms (6 decimals)
      example: 1000000

    UnixTimestamp:
      type: integer
      description: Seconds since Unix epoch

    EDcaPlanPeriod:
      type: string
      enum: [hourly, semidaily, daily, weekly, biweekly, monthly]

    EDcaPlanStatus:
      type: string
      enum: [Active, Paused, Error]

    EDcaPlanNoticeKind:
      type: string
      enum: [None, RetryScheduled, NeedsFunding]

    EDcaPlanToToken:
      type: string
      enum: [native, bitcoin, paxg]

    EDcaTransferStatus:
      type: string
      enum:
        - Started
        - BridgeSubmitted
        - BridgeCompleted
        - PurchaseSubmitted
        - Confirmed
        - Failed

    EPriceAsset:
      type: string
      enum: [eth, btc, paxg]

    ETokenKind:
      type: string
      enum: [erc20, native, bitcoin]

    EChainNetworkKind:
      type: string
      enum: [evm, bitcoin]

    EDcaPlanAddressKind:
      type: string
      enum: [evm, bitcoin]

    User:
      type: object
      required: [id, accountAddress, timezone]
      properties:
        id:
          type: string
          format: uuid
        accountAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        timezone:
          type: string
          example: America/Los_Angeles

    SiweNonceResponse:
      type: object
      required: [nonce, chainId, domain]
      properties:
        nonce:
          type: string
        chainId:
          type: integer
        domain:
          type: string

    SiweVerifyRequest:
      type: object
      required: [message, signature]
      properties:
        message:
          type: string
          minLength: 1
        signature:
          type: string
          minLength: 1

    SiweVerifyResponse:
      type: object
      required: [user]
      properties:
        user:
          $ref: "#/components/schemas/User"

    SiweTokenResponse:
      type: object
      required: [user, accessToken, tokenType, expiresIn, signerNonce]
      properties:
        user:
          $ref: "#/components/schemas/User"
        accessToken:
          type: string
          description: JWT (HS256)
        tokenType:
          type: string
          enum: [Bearer]
        expiresIn:
          type: integer
          description: TTL seconds
          example: 86400
        signerNonce:
          type: string
          nullable: true
          description: |
            Additional-signer memo correlation nonce (`0x` + 32 hex). Present when
            the SIWE address is a registered additional signer; null for primary login.
            For Agent Activity on the owner dashboard, pass on every
            `relayer_send7710Transaction*` alongside
            `destinationUrl: "https://coinoperated.finance/api/webhooks/relayer"`:

            ```json
            {
              "source": "coinoperated",
              "signerAddress": "<agent 0x address>",
              "nonce": "<this signerNonce>",
              "justification": "optional UI label"
            }
            ```

            `params.memo` must be that JSON string (≤256 chars). Omitting `source`,
            `signerAddress`, or `nonce` skips ingest (`GET /api/user/agent-activity`
            stays empty).

    SessionStatusResponse:
      type: object
      required: [session]
      properties:
        session:
          type: boolean
        user:
          $ref: "#/components/schemas/User"

    UserContactInfo:
      type: object
      required:
        - id
        - userId
        - emailAddress
        - phoneNumber
        - telegramHandle
        - primaryContact
        - verified
        - telegramStartUrl
        - verifiedTimestamp
        - marketingOptIn
        - activityOptIn
        - systemMessagesOptIn
        - createdTimestamp
        - updatedTimestamp
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        emailAddress:
          type: string
          nullable: true
        phoneNumber:
          type: string
          nullable: true
        telegramHandle:
          type: string
          nullable: true
        primaryContact:
          type: boolean
        verified:
          type: boolean
        telegramStartUrl:
          type: string
          nullable: true
        verifiedTimestamp:
          allOf:
            - $ref: "#/components/schemas/UnixTimestamp"
          nullable: true
        marketingOptIn:
          type: boolean
        activityOptIn:
          type: boolean
        systemMessagesOptIn:
          type: boolean
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        updatedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"

    AddContactRequest:
      type: object
      properties:
        emailAddress:
          type: string
          format: email
        telegramHandle:
          type: string
      description: Exactly one of emailAddress or telegramHandle

    AdditionalSigner:
      type: object
      required:
        - id
        - userId
        - accountAddress
        - label
        - createdTimestamp
        - updatedTimestamp
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        accountAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        label:
          type: string
          nullable: true
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        updatedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"

    AgentActivity:
      type: object
      required:
        - id
        - userId
        - additionalSignerId
        - signerAccountAddress
        - signerLabel
        - relayerTransactionId
        - chainId
        - justification
        - transactionHash
        - status
        - createdTimestamp
        - updatedTimestamp
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        additionalSignerId:
          type: string
          format: uuid
        signerAccountAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        signerLabel:
          type: string
          nullable: true
        relayerTransactionId:
          type: string
          description: Relayer task id (0x + 64 hex)
        chainId:
          type: string
        justification:
          type: string
        transactionHash:
          type: string
          nullable: true
        status:
          type: string
          enum: [submitted, completed, failed]
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        updatedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"

    AgentDelegation:
      type: object
      required:
        - id
        - userId
        - additionalSignerId
        - delegateAccountAddress
        - chainId
        - tokenAddress
        - periodAmount
        - period
        - periodDuration
        - delegation
        - permissionContext
        - createdTimestamp
        - updatedTimestamp
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        additionalSignerId:
          type: string
          format: uuid
        delegateAccountAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        chainId:
          type: string
        tokenAddress:
          $ref: "#/components/schemas/EVMContractAddress"
        periodAmount:
          type: integer
          description: USDC atoms (6 decimals)
        period:
          $ref: "#/components/schemas/EDcaPlanPeriod"
        periodDuration:
          type: integer
          description: Period length in seconds
        delegation:
          type: string
          description: Serialized MetaMask Delegation JSON
        permissionContext:
          $ref: "#/components/schemas/HexString"
        justification:
          type: string
          nullable: true
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        updatedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"

    DcaPlan:
      type: object
      required:
        - id
        - name
        - description
        - period
        - amountPerPeriod
        - toChainId
        - toToken
        - planTypeId
        - toAddress
        - hops
        - status
        - noticeKind
        - nextExecutionTimestamp
        - createdTimestamp
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        period:
          $ref: "#/components/schemas/EDcaPlanPeriod"
        amountPerPeriod:
          $ref: "#/components/schemas/USDCAmount"
        toChainId:
          type: string
        toToken:
          $ref: "#/components/schemas/EDcaPlanToToken"
        planTypeId:
          type: string
        toAddress:
          type: string
        hops:
          type: integer
          enum: [1, 2]
        status:
          $ref: "#/components/schemas/EDcaPlanStatus"
        noticeKind:
          $ref: "#/components/schemas/EDcaPlanNoticeKind"
        nextExecutionTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        updatedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"

    DcaPlanCreateConfig:
      type: object
      properties:
        sourceQuoteSignerAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        bridgeQuoteSignerAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        sourceLifiDiamondAddress:
          $ref: "#/components/schemas/EVMContractAddress"
        sourceChainId:
          $ref: "#/components/schemas/EVMChainId"
        sourceToken:
          $ref: "#/components/schemas/EVMContractAddress"
        sourceTargetAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        sourceFeeCollector:
          $ref: "#/components/schemas/EVMAccountAddress"
        bridgeLifiDiamondAddress:
          $ref: "#/components/schemas/EVMContractAddress"
        bridgeChainId:
          $ref: "#/components/schemas/EVMChainId"
        bridgeToken:
          $ref: "#/components/schemas/EVMContractAddress"
        bridgeTargetAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        bridgeFeeCollector:
          $ref: "#/components/schemas/EVMAccountAddress"
        lifiSwapEnforcerAddress:
          $ref: "#/components/schemas/EVMContractAddress"
        sourceRelayerFeeUsdcAtoms:
          type: integer
        bridgeRelayerFeeUsdcAtoms:
          type: integer

    ListPlansResponse:
      type: object
      required: [response, page, pageSize, totalResults, config]
      properties:
        response:
          type: array
          items:
            $ref: "#/components/schemas/DcaPlan"
        page:
          type: integer
        pageSize:
          type: integer
        totalResults:
          type: integer
        config:
          $ref: "#/components/schemas/DcaPlanCreateConfig"

    CreatePlanRequest:
      type: object
      required:
        - name
        - description
        - planTypeId
        - delegateAccountAddress
        - toAddress
        - amountPerPeriod
        - period
        - approveDelegation
        - feeDelegation
        - swapDelegation
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
        description:
          type: string
        planTypeId:
          type: string
        delegateAccountAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        toAddress:
          type: string
          description: EVM address or Bitcoin SegWit
        amountPerPeriod:
          type: integer
          minimum: 1
        period:
          $ref: "#/components/schemas/EDcaPlanPeriod"
        approveDelegation:
          type: string
          description: Signed delegation JSON string
        feeDelegation:
          type: string
        swapDelegation:
          type: string
        bridgeChainApproveDelegation:
          type: string
          nullable: true
        bridgeChainFeeDelegation:
          type: string
          nullable: true
        bridgeChainSwapDelegation:
          type: string
          nullable: true

    PlanHoldingsStats:
      type: object
      properties:
        costBasisUsdcAtoms:
          type: string
        accumulatedAtoms:
          type: string
        accumulatedValueUsdcAtoms:
          type: string
          nullable: true

    PlanDetailResponse:
      oneOf:
        - $ref: "#/components/schemas/DcaPlanDetailResponse"
        - $ref: "#/components/schemas/ResolvedTriggeredPlanDetailResponse"
      discriminator:
        propertyName: kind
        mapping:
          dca: "#/components/schemas/DcaPlanDetailResponse"
          triggered: "#/components/schemas/ResolvedTriggeredPlanDetailResponse"

    DcaPlanDetailResponse:
      type: object
      required: [kind, plan, stats]
      properties:
        kind:
          type: string
          enum: [dca]
        plan:
          $ref: "#/components/schemas/DcaPlan"
        stats:
          $ref: "#/components/schemas/PlanHoldingsStats"

    ResolvedTriggeredPlanDetailResponse:
      type: object
      required: [kind, plan, stats]
      properties:
        kind:
          type: string
          enum: [triggered]
        plan:
          $ref: "#/components/schemas/TriggeredPlan"
        stats:
          $ref: "#/components/schemas/PlanHoldingsStats"

    PlanCancelItem:
      type: object
      required: [chainId, permissionContext]
      properties:
        chainId:
          $ref: "#/components/schemas/EVMChainId"
        permissionContext:
          $ref: "#/components/schemas/HexString"

    TriggeredPlan:
      type: object
      required:
        - id
        - userId
        - name
        - description
        - delegateAccountAddress
        - fromChainId
        - fromToken
        - toChainId
        - toToken
        - planTypeId
        - toAddress
        - hops
        - amountPerPeriod
        - period
        - ruleKind
        - expectedDecimals
        - maxStaleSeconds
        - minGapSeconds
        - priceFeed
        - status
        - noticeKind
        - nextEligibleTimestamp
        - createdTimestamp
        - updatedTimestamp
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        delegateAccountAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        fromChainId:
          type: string
        fromToken:
          type: string
        toChainId:
          type: string
        toToken:
          $ref: "#/components/schemas/EDcaPlanToToken"
        planTypeId:
          type: string
        toAddress:
          type: string
        hops:
          type: integer
          enum: [1, 2]
        amountPerPeriod:
          $ref: "#/components/schemas/USDCAmount"
        period:
          $ref: "#/components/schemas/EDcaPlanPeriod"
        ruleKind:
          type: integer
          enum: [0, 1, 2, 3]
          description: "0 Dip, 1 Rise, 2 AbsoluteGte, 3 AbsoluteLte"
        thresholdBps:
          type: integer
          nullable: true
        windowSeconds:
          type: integer
          nullable: true
        triggerPrice:
          type: string
          nullable: true
        expectedDecimals:
          type: integer
        maxStaleSeconds:
          type: integer
        minGapSeconds:
          type: integer
        priceFeed:
          $ref: "#/components/schemas/EVMContractAddress"
        status:
          $ref: "#/components/schemas/EDcaPlanStatus"
        noticeKind:
          $ref: "#/components/schemas/EDcaPlanNoticeKind"
        nextEligibleTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        updatedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"

    TriggeredPlanCreateConfig:
      allOf:
        - $ref: "#/components/schemas/DcaPlanCreateConfig"
        - type: object
          properties:
            chainlinkPriceRuleEnforcerAddress:
              $ref: "#/components/schemas/EVMContractAddress"
            ethPriceFeed:
              $ref: "#/components/schemas/EVMContractAddress"
            btcPriceFeed:
              $ref: "#/components/schemas/EVMContractAddress"
            paxgPriceFeed:
              $ref: "#/components/schemas/EVMContractAddress"

    ListTriggeredPlansResponse:
      type: object
      required: [response, page, pageSize, totalResults, config]
      properties:
        response:
          type: array
          items:
            $ref: "#/components/schemas/TriggeredPlan"
        page:
          type: integer
        pageSize:
          type: integer
        totalResults:
          type: integer
        config:
          $ref: "#/components/schemas/TriggeredPlanCreateConfig"

    CreateTriggeredPlanRequest:
      allOf:
        - $ref: "#/components/schemas/CreatePlanRequest"
        - type: object
          required:
            - ruleKind
            - expectedDecimals
            - maxStaleSeconds
            - minGapSeconds
            - priceFeed
          properties:
            ruleKind:
              type: integer
              enum: [0, 1, 2, 3]
            thresholdBps:
              type: integer
              nullable: true
              minimum: 0
              maximum: 10000
            windowSeconds:
              type: integer
              nullable: true
              minimum: 1
            triggerPrice:
              type: string
              nullable: true
            expectedDecimals:
              type: integer
              minimum: 2
              maximum: 18
            maxStaleSeconds:
              type: integer
              minimum: 1
            minGapSeconds:
              type: integer
              minimum: 0
            priceFeed:
              $ref: "#/components/schemas/EVMContractAddress"

    TriggeredPlanDetailResponse:
      type: object
      required: [plan, stats]
      properties:
        plan:
          $ref: "#/components/schemas/TriggeredPlan"
        stats:
          $ref: "#/components/schemas/PlanHoldingsStats"

    PriceEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        asset:
          $ref: "#/components/schemas/EPriceAsset"
        roundId:
          type: string
        priceCents:
          type: integer
        quotedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"

    DcaTransfer:
      type: object
      required:
        - id
        - planId
        - triggeredPlanId
        - status
        - amount
        - feeAmount
        - expectedAmountReceived
        - actualAmountReceived
        - expectedSlippageBps
        - actualSlippageBps
        - priceEntryId
        - includedApprove
        - errorMessage
        - createdTimestamp
        - updatedTimestamp
        - completedTimestamp
      properties:
        id:
          type: string
          format: uuid
        planId:
          type: string
          format: uuid
          nullable: true
        triggeredPlanId:
          type: string
          format: uuid
          nullable: true
        status:
          $ref: "#/components/schemas/EDcaTransferStatus"
        amount:
          $ref: "#/components/schemas/USDCAmount"
        feeAmount:
          $ref: "#/components/schemas/USDCAmount"
        expectedAmountReceived:
          type: string
        actualAmountReceived:
          type: string
          nullable: true
        expectedSlippageBps:
          type: number
        actualSlippageBps:
          type: number
          nullable: true
        priceEntryId:
          type: string
          format: uuid
          nullable: true
        includedApprove:
          type: boolean
        errorMessage:
          type: string
          nullable: true
        expectedIntegrationFee:
          $ref: "#/components/schemas/USDCAmount"
        actualIntegrationFee:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        expectedLifiFee:
          $ref: "#/components/schemas/USDCAmount"
        actualLifiFee:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        bridgeFeeAmount:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        purchaseFeeAmount:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        purchaseExpectedIntegrationFee:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        purchaseActualIntegrationFee:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        purchaseExpectedLifiFee:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        purchaseActualLifiFee:
          allOf:
            - $ref: "#/components/schemas/USDCAmount"
          nullable: true
        bridgeTransactionHash:
          type: string
          nullable: true
        purchaseTransactionHash:
          type: string
          nullable: true
        purchaseExpectedAmountReceived:
          type: string
          nullable: true
        purchaseActualAmountReceived:
          type: string
          nullable: true
        createdTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        updatedTimestamp:
          $ref: "#/components/schemas/UnixTimestamp"
        completedTimestamp:
          allOf:
            - $ref: "#/components/schemas/UnixTimestamp"
          nullable: true
        priceEntry:
          allOf:
            - $ref: "#/components/schemas/PriceEntry"
          nullable: true

    PagedTransfers:
      type: object
      required: [response, page, pageSize, totalResults]
      properties:
        response:
          type: array
          items:
            $ref: "#/components/schemas/DcaTransfer"
        page:
          type: integer
        pageSize:
          type: integer
        totalResults:
          type: integer

    PriceBucket:
      type: object
      required: [asset, range, bucketStart, openCents, highCents, lowCents, closeCents]
      properties:
        asset:
          $ref: "#/components/schemas/EPriceAsset"
        range:
          type: string
        bucketStart:
          $ref: "#/components/schemas/UnixTimestamp"
        openCents:
          type: integer
        highCents:
          type: integer
        lowCents:
          type: integer
        closeCents:
          type: integer

    TokenInfo:
      type: object
      properties:
        id:
          type: string
        chainInfoId:
          type: string
        symbol:
          type: string
        name:
          type: string
        decimals:
          type: integer
        tokenKind:
          $ref: "#/components/schemas/ETokenKind"
        contractAddress:
          allOf:
            - $ref: "#/components/schemas/EVMContractAddress"
          nullable: true
        lifiAssetId:
          type: string
          nullable: true
        isStablecoin:
          type: boolean

    ChainInfo:
      type: object
      properties:
        id:
          type: string
        lifiChainId:
          type: string
        evmChainIdHex:
          type: string
          nullable: true
        owsChainId:
          type: string
          nullable: true
        name:
          type: string
        shortName:
          type: string
        networkKind:
          $ref: "#/components/schemas/EChainNetworkKind"
        lifiDiamondAddress:
          allOf:
            - $ref: "#/components/schemas/EVMContractAddress"
          nullable: true
        defaultRelayerFeeUsdcAtoms:
          type: integer
          nullable: true
        defaultQuoteSignerAddress:
          allOf:
            - $ref: "#/components/schemas/EVMAccountAddress"
          nullable: true
        defaultQuoteSignerWalletId:
          type: string
          nullable: true
        explorerTxBaseUrl:
          type: string
          nullable: true

    DcaPlanType:
      type: object
      properties:
        id:
          type: string
        toToken:
          $ref: "#/components/schemas/EDcaPlanToToken"
        hops:
          type: integer
          enum: [1, 2]
        sourceTokenId:
          type: string
        bridgeTokenId:
          type: string
          nullable: true
        destinationTokenId:
          type: string
        hop1DenyBridges:
          type: array
          items:
            type: string
        hop1AllowBridges:
          type: array
          items:
            type: string
          nullable: true
        hop2AllowBridges:
          type: array
          items:
            type: string
          nullable: true
        hop1SlippageBps:
          type: integer
        purchaseSlippageBps:
          type: integer
        title:
          type: string
        description:
          type: string
        weight:
          type: integer
        addressKind:
          $ref: "#/components/schemas/EDcaPlanAddressKind"
        sourceToken:
          $ref: "#/components/schemas/TokenInfo"
        destinationToken:
          $ref: "#/components/schemas/TokenInfo"
        bridgeToken:
          allOf:
            - $ref: "#/components/schemas/TokenInfo"
          nullable: true

    RelayerCapabilities:
      type: object
      properties:
        chainId:
          type: string
        targetAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        feeCollector:
          $ref: "#/components/schemas/EVMAccountAddress"
        sourceQuoteSignerAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        bridgeQuoteSignerAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        sourceLifiDiamondAddress:
          $ref: "#/components/schemas/EVMContractAddress"
        sourceToken:
          $ref: "#/components/schemas/EVMContractAddress"
        sourceChainId:
          $ref: "#/components/schemas/EVMChainId"
        bridgeLifiDiamondAddress:
          $ref: "#/components/schemas/EVMContractAddress"
        bridgeChainId:
          $ref: "#/components/schemas/EVMChainId"
        bridgeToken:
          $ref: "#/components/schemas/EVMContractAddress"
        bridgeTargetAddress:
          $ref: "#/components/schemas/EVMAccountAddress"
        bridgeFeeCollector:
          $ref: "#/components/schemas/EVMAccountAddress"
        lifiSwapEnforcerAddress:
          $ref: "#/components/schemas/EVMContractAddress"
        sourceRelayerFeeUsdcAtoms:
          type: integer
        bridgeRelayerFeeUsdcAtoms:
          type: integer

    DcaSimulateRequest:
      type: object
      required: [asset, amount, cadence, startDate, endDate, lumpSum]
      properties:
        asset:
          type: string
          enum: [btc, eth, paxg]
        amount:
          type: number
          exclusiveMinimum: 0
        cadence:
          type: string
          enum: [hourly, semidaily, daily, weekly, biweekly, monthly]
        startDate:
          type: string
          format: date
        endDate:
          type: string
          format: date
        lumpSum:
          type: boolean
        includeHub:
          type: boolean
        strategy:
          type: string
          enum: [dca, dip]
          default: dca
        dipPercent:
          type: number
          minimum: 0.01
          maximum: 99.99
          default: 1
          description: Buy-the-dip threshold. 1 means a 1% drop from the lookback-start price.
        dipWindowSeconds:
          type: integer
          enum: [3600, 21600, 86400, 604800]
          default: 86400

    DcaSimulateResponse:
      type: object
      description: Historical DCA simulator payload (shape varies by outcome)
      additionalProperties: true
      properties:
        outcome:
          type: object
          additionalProperties: true
        hub:
          type: object
          nullable: true
          additionalProperties: true
        bounds:
          type: object
          properties:
            earliest:
              type: string
            latest:
              type: string
        payload:
          type: object
          nullable: true
          additionalProperties: true

    RelayerWebhookBody:
      type: object
      required: [signature, keyId, type, timestamp, data]
      properties:
        apiVersion:
          type: integer
        signature:
          type: string
        keyId:
          type: string
        type:
          type: integer
          enum: [0, 1, 4]
        timestamp:
          type: integer
        data:
          type: object
          required: [id, chainId, createdAt, status]
          properties:
            id:
              type: string
            chainId:
              type: string
            createdAt:
              type: integer
            status:
              type: integer
              enum: [100, 110, 200, 400, 500]
            hash:
              type: string
            message:
              type: string
            data:
              description: Opaque relayer context
            receipt:
              type: object
              additionalProperties: true
