openapi: 3.1.0
info:
  title: VAYPEER API
  version: 1.0.0-preview
  description: |
    Contract-first API for VAYPEER marketplace automation. This file describes the
    target service. A local Fastify implementation now covers wallet sessions, offers,
    quotes, trade transitions, disputes, staff moderation, scoped developer credentials,
    replay-safe HMAC reads and mutations, durable idempotency responses, transactional
    webhook outbox processing, and signed delivery; no production API is deployed.
servers:
  - url: https://sandbox-api.pnlstake.com/v1
    description: Planned sandbox
  - url: https://api.pnlstake.com/v1
    description: Planned production
tags:
  - name: Markets
  - name: Bank accounts
  - name: Payment methods
  - name: Profile
  - name: Realtime
  - name: Offers
  - name: Gift cards
  - name: Trades
  - name: Messages
  - name: Disputes
  - name: Staff
  - name: Developer
  - name: Webhooks
paths:
  /market/reference-price:
    get:
      tags: [Markets]
      operationId: getMarketReferencePrice
      summary: Get a cached Bybit-based crypto price in the selected fiat currency
      parameters:
        - {name: asset, in: query, required: true, schema: {type: string, enum: [BNB, USDT]}}
        - {name: fiat, in: query, required: true, schema: {type: string, pattern: '^[A-Za-z]{3}$'}}
      responses:
        '200': {description: Live or stale-safe market reference, content: {application/json: {schema: {$ref: '#/components/schemas/MarketReferencePrice'}}}}
        '503': {$ref: '#/components/responses/ServiceUnavailable'}
  /bank-accounts:
    get:
      tags: [Bank accounts]
      operationId: listParticipantBankAccounts
      security: [{UserCookie: []}]
      responses:
        '200':
          description: Active payout accounts with masked account numbers
          content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/ParticipantBankAccount'}}}}}}
    post:
      tags: [Bank accounts]
      operationId: createParticipantBankAccount
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/CreateParticipantBankAccount'}}}
      responses:
        '201': {description: Account created, content: {application/json: {schema: {$ref: '#/components/schemas/ParticipantBankAccount'}}}}
  /bank-accounts/{bankAccountId}:
    parameters:
      - {name: bankAccountId, in: path, required: true, schema: {type: string, format: uuid}}
    patch:
      tags: [Bank accounts]
      operationId: setDefaultParticipantBankAccount
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content: {application/json: {schema: {type: object, additionalProperties: false, required: [isDefault], properties: {isDefault: {type: boolean, const: true}}}}}
      responses:
        '200': {description: Default account updated, content: {application/json: {schema: {$ref: '#/components/schemas/ParticipantBankAccount'}}}}
    delete:
      tags: [Bank accounts]
      operationId: archiveParticipantBankAccount
      summary: Archive an account and pause active offers that depend on it
      security: [{UserCookie: []}]
      responses:
        '204': {description: Account archived}
  /profile:
    patch:
      tags: [Profile]
      operationId: updateParticipantProfile
      summary: Set or change the authenticated wallet profile username
      description: A username may be changed once every 365 days. Reusing the current username is idempotent.
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [username]
              properties:
                username: {type: string, pattern: '^[A-Za-z0-9_]{3,24}$'}
      responses:
        '200': {description: Updated profile, content: {application/json: {schema: {$ref: '#/components/schemas/ParticipantProfile'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /profile/photo:
    post:
      tags: [Profile]
      operationId: uploadParticipantProfilePhoto
      summary: Replace the authenticated wallet profile photo
      security: [{UserCookie: []}]
      parameters:
        - name: X-File-Content-Type
          in: header
          required: true
          schema: {type: string, enum: [image/jpeg, image/png, image/webp]}
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: {type: string, format: binary, maxLength: 5242880}
      responses:
        '201': {description: Updated profile, content: {application/json: {schema: {$ref: '#/components/schemas/ParticipantProfile'}}}}
  /profiles/{userId}/photo:
    get:
      tags: [Profile]
      operationId: getParticipantProfilePhoto
      summary: Read the current public profile photo
      parameters: [{$ref: '#/components/parameters/UserId'}]
      responses:
        '200': {description: JPEG, PNG, or WebP image, content: {image/jpeg: {schema: {type: string, format: binary}}, image/png: {schema: {type: string, format: binary}}, image/webp: {schema: {type: string, format: binary}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /trader-profiles/{identifier}:
    get:
      tags: [Profile]
      operationId: getPublicTraderProfile
      summary: Get a trader's public reputation, block totals, feedback, and active offers
      parameters:
        - name: identifier
          in: path
          required: true
          schema: {type: string, minLength: 1, maxLength: 64}
      responses:
        '200': {description: Public trader profile, content: {application/json: {schema: {$ref: '#/components/schemas/PublicTraderProfile'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /trader-profiles/{userId}/block:
    parameters: [{$ref: '#/components/parameters/UserId'}]
    get:
      tags: [Profile]
      operationId: getTraderBlockStatus
      security: [{UserCookie: []}]
      responses:
        '200': {description: Current block state, content: {application/json: {schema: {$ref: '#/components/schemas/TraderBlockState'}}}}
    put:
      tags: [Profile]
      operationId: blockTrader
      security: [{UserCookie: []}]
      responses:
        '200': {description: Trader blocked, content: {application/json: {schema: {$ref: '#/components/schemas/TraderBlockState'}}}}
    delete:
      tags: [Profile]
      operationId: unblockTrader
      security: [{UserCookie: []}]
      responses:
        '200': {description: Trader unblocked, content: {application/json: {schema: {$ref: '#/components/schemas/TraderBlockState'}}}}
  /events:
    get:
      tags: [Realtime]
      operationId: listParticipantEvents
      summary: Replay the connected wallet's durable trade events
      security: [{UserCookie: []}]
      parameters:
        - name: after
          in: query
          schema: {type: integer, minimum: 0, default: 0}
        - name: limit
          in: query
          schema: {type: integer, minimum: 1, maximum: 200, default: 100}
      responses:
        '200':
          description: Participant event page
          content:
            application/json:
              schema:
                type: object
                required: [data, nextAfter]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/ParticipantEvent'}}
                  nextAfter: {type: integer, minimum: 0}
  /events/stream:
    get:
      tags: [Realtime]
      operationId: streamParticipantEvents
      summary: Stream durable participant events with Server-Sent Events
      security: [{UserCookie: []}]
      parameters:
        - name: after
          in: query
          schema: {type: integer, minimum: 0, default: 0}
        - name: Last-Event-ID
          in: header
          schema: {type: integer, minimum: 0}
      responses:
        '200':
          description: Reconnectable participant event stream
          content:
            text/event-stream:
              schema: {type: string}
  /push/config:
    get:
      tags: [Realtime]
      operationId: getWebPushConfig
      summary: Read the browser Web Push public configuration
      responses:
        '200':
          description: Web Push availability and VAPID public key
          content:
            application/json:
              schema:
                type: object
                required: [enabled, publicKey]
                properties:
                  enabled: {type: boolean}
                  publicKey: {type: [string, 'null']}
  /push/subscriptions:
    post:
      tags: [Realtime]
      operationId: registerWebPushSubscription
      summary: Register or refresh an encrypted device subscription
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PushSubscriptionRequest'}
      responses:
        '201': {description: Device registered}
    delete:
      tags: [Realtime]
      operationId: revokeWebPushSubscription
      summary: Revoke the current device subscription
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [endpoint]
              properties:
                endpoint: {type: string, format: uri, maxLength: 4096}
      responses:
        '204': {description: Device revoked}
  /payment-methods:
    get:
      tags: [Payment methods]
      operationId: listPaymentMethods
      summary: List active administrator-managed payment methods
      responses:
        '200':
          description: Active payment methods
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/PaymentMethod'}}
  /markets:
    get:
      tags: [Markets]
      operationId: listMarkets
      summary: List enabled asset, chain, currency, and payment-method markets
      responses:
        '200':
          description: Market configuration
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: {$ref: '#/components/schemas/Market'}
  /protocol/stats:
    get:
      tags: [Markets]
      operationId: getProtocolStats
      summary: Get indexer-derived protocol statistics
      responses:
        '200':
          description: Confirmed trade statistics and observed chain block
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProtocolStats'}
  /offers:
    get:
      tags: [Offers]
      operationId: listOffers
      summary: Search live, backed offers
      parameters:
        - {$ref: '#/components/parameters/Asset'}
        - {$ref: '#/components/parameters/Fiat'}
        - name: side
          in: query
          schema: {type: string, enum: [BUY, SELL]}
        - name: paymentMethod
          in: query
          schema: {type: string}
        - name: amount
          in: query
          schema: {type: string, pattern: '^\d+(\.\d+)?$'}
        - {$ref: '#/components/parameters/Cursor'}
      responses:
        '200':
          description: Offer page
          content:
            application/json:
              schema:
                type: object
                required: [data, page]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/Offer'}}
                  page: {$ref: '#/components/schemas/CursorPage'}
    post:
      tags: [Offers]
      operationId: createOffer
      summary: Create a backed offer
      security: [{HmacAuth: []}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      x-required-scope: offers:write
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateOfferRequest'}
      responses:
        '201':
          description: Offer accepted after vault eligibility checks
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Offer'}
        '409': {$ref: '#/components/responses/Conflict'}
  /offers/{offerId}:
    parameters: [{$ref: '#/components/parameters/OfferId'}]
    get:
      tags: [Offers]
      operationId: getOffer
      summary: Get a live offer
      responses:
        '200':
          description: Offer
          content: {application/json: {schema: {$ref: '#/components/schemas/Offer'}}}
        '404': {$ref: '#/components/responses/NotFound'}
    patch:
      tags: [Offers]
      operationId: updateOffer
      summary: Update mutable offer configuration
      security: [{HmacAuth: []}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      x-required-scope: offers:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                status: {type: string, enum: [ACTIVE, PAUSED, HIDDEN, CLOSED]}
                marginBps: {type: integer, minimum: -5000, maximum: 5000}
                minimumFiat: {type: string}
                maximumFiat: {type: string}
      responses:
        '200':
          description: Updated offer
          content: {application/json: {schema: {$ref: '#/components/schemas/Offer'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /trade-quotes:
    post:
      tags: [Trades]
      operationId: createTradeQuote
      summary: Preview exact terms without opening a trade
      security: [{HmacAuth: []}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      x-required-scope: trades:create
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateTradeQuoteRequest'}
      responses:
        '201':
          description: Short-lived confirmation quote; no liquidity has been reserved
          content: {application/json: {schema: {$ref: '#/components/schemas/TradeQuote'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /trades:
    get:
      tags: [Trades]
      operationId: listTrades
      summary: List the wallet's active or completed trades
      security: [{HmacAuth: []}]
      x-required-scope: trades:read
      parameters:
        - name: state
          in: query
          schema: {type: string, enum: [active, completed], default: active}
        - {$ref: '#/components/parameters/Cursor'}
      responses:
        '200':
          description: Trade page
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/Trade'}}
                  page: {$ref: '#/components/schemas/CursorPage'}
    post:
      tags: [Trades]
      operationId: createTrade
      summary: Open a trade and request on-chain liquidity reservation
      security: [{HmacAuth: []}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      x-required-scope: trades:create
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirmationToken, buyerWallet]
              additionalProperties: false
              properties:
                confirmationToken: {type: string, minLength: 32}
                buyerWallet: {type: string, pattern: '^0x[a-fA-F0-9]{40}$'}
      responses:
        '202':
          description: Reservation submitted; watch state until chain confirmation
          content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /trades/{tradeId}:
    get:
      tags: [Trades]
      operationId: getTrade
      summary: Get current projected and confirmed trade state
      security: [{HmacAuth: []}]
      x-required-scope: trades:read
      parameters: [{$ref: '#/components/parameters/TradeId'}]
      responses:
        '200':
          description: Trade
          content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /trades/{tradeId}/feedback:
    post:
      tags: [Profile, Trades]
      operationId: createTradeFeedback
      summary: Leave one feedback entry for the counterparty after a completed trade
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/TradeId'}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [rating]
              properties:
                rating: {type: string, enum: [POSITIVE, NEGATIVE]}
                comment: {type: string, maxLength: 500, default: ''}
      responses:
        '201': {description: Feedback recorded, content: {application/json: {schema: {$ref: '#/components/schemas/PublicTradeFeedback'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /trades/{tradeId}/cancel:
    post:
      tags: [Trades]
      operationId: cancelTrade
      summary: Cancel as buyer before payment is marked
      description: Sellers cannot cancel while waiting for buyer payment. Cancellation is locked for everyone after payment is marked.
      security: [{HmacAuth: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      x-required-scope: trades:create
      responses:
        '202':
          description: Cancellation accepted
          content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /trades/{tradeId}/paid:
    post:
      tags: [Trades]
      operationId: markTradePaid
      summary: Mark fiat payment as sent
      description: Does not prove fiat settlement and does not release crypto.
      security: [{HmacAuth: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      x-required-scope: trades:mark_paid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [paymentReference, paidAt]
              properties:
                paymentReference: {type: string, minLength: 3, maxLength: 120}
                paidAt: {type: string, format: date-time}
                note: {type: string, maxLength: 800}
      responses:
        '202':
          description: State transition accepted
          content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /chain/status:
    get:
      tags: [Trades]
      operationId: getChainReconciliationStatus
      summary: Read the configured BNB Chain confirmation policy
      responses:
        '200':
          description: Chain reconciliation availability
          content: {application/json: {schema: {$ref: '#/components/schemas/ChainStatus'}}}
  /trades/{tradeId}/chain/escrow-funding:
    get:
      tags: [Trades]
      operationId: getEscrowFundingTransaction
      summary: Read canonical escrow funding progress
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/TradeId'}]
      responses:
        '200':
          description: Latest escrow funding transaction, if submitted
          content:
            application/json:
              schema:
                type: object
                required: [transaction]
                properties:
                  transaction: {oneOf: [{$ref: '#/components/schemas/ChainTransaction'}, {type: 'null'}]}
    post:
      tags: [Trades]
      operationId: submitEscrowFundingTransaction
      summary: Register the crypto seller's BNB Chain escrow transaction
      description: Acceptance does not mean escrow is funded. Trade state changes only after the exact TradeFunded event reaches the configured confirmation threshold.
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/TradeId'}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [transactionHash]
              properties:
                transactionHash: {type: string, pattern: '^0x[a-fA-F0-9]{64}$'}
      responses:
        '202':
          description: Transaction accepted for confirmation tracking
          content: {application/json: {schema: {$ref: '#/components/schemas/ChainTransaction'}}}
        '409': {$ref: '#/components/responses/Conflict'}
        '503': {$ref: '#/components/responses/ServiceUnavailable'}
  /trades/{tradeId}/chain/escrow-action:
    get:
      tags: [Trades]
      operationId: prepareEscrowFundingAction
      summary: Prepare seller-executed approval and escrow transactions
      description: Returns calldata only to the authenticated crypto seller. The admission signature is bound to the exact trade, nonce, chain, escrow, token, amounts, fee policy, hashes, funding source, and short authorization deadline.
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/TradeId'}]
      responses:
        '200':
          description: Wallet transactions ready for execution
          content: {application/json: {schema: {$ref: '#/components/schemas/PreparedEscrowFundingAction'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
        '503': {$ref: '#/components/responses/ServiceUnavailable'}
  /trades/{tradeId}/chain/actions/{action}:
    get:
      tags: [Trades]
      operationId: prepareTradeChainAction
      summary: Prepare canonical BNB Chain lifecycle calldata
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {name: action, in: path, required: true, schema: {type: string, enum: [mark-payment, cancel, expire, release, dispute]}}
      responses:
        '200':
          description: Wallet transaction ready for execution
          content: {application/json: {schema: {$ref: '#/components/schemas/PreparedChainLifecycleAction'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
        '503': {$ref: '#/components/responses/ServiceUnavailable'}
  /trades/{tradeId}/chain/actions/{action}/transaction:
    get:
      tags: [Trades]
      operationId: getTradeChainActionTransaction
      summary: Read canonical lifecycle transaction progress
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {name: action, in: path, required: true, schema: {type: string, enum: [mark-payment, cancel, expire, release, dispute]}}
      responses:
        '200':
          description: Latest transaction, if submitted
          content:
            application/json:
              schema:
                type: object
                required: [transaction]
                properties:
                  transaction: {oneOf: [{$ref: '#/components/schemas/ChainTransaction'}, {type: 'null'}]}
  /trades/{tradeId}/chain/actions/{action}/transactions:
    post:
      tags: [Trades]
      operationId: submitTradeChainActionTransaction
      summary: Register a participant lifecycle transaction
      description: State changes only after the expected event from the configured escrow reaches finality.
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {name: action, in: path, required: true, schema: {type: string, enum: [mark-payment, cancel, expire, release, dispute]}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [transactionHash]
              properties:
                transactionHash: {type: string, pattern: '^0x[a-fA-F0-9]{64}$'}
                dispute: {$ref: '#/components/schemas/OpenDisputeRequest'}
      responses:
        '202':
          description: Transaction accepted for confirmation tracking
          content: {application/json: {schema: {$ref: '#/components/schemas/ChainTransaction'}}}
        '409': {$ref: '#/components/responses/Conflict'}
        '503': {$ref: '#/components/responses/ServiceUnavailable'}
  /trades/{tradeId}/release-intents:
    post:
      tags: [Trades]
      operationId: createReleaseIntent
      summary: Validate an exact seller-signed release intent
      description: A general API secret cannot release escrow. This endpoint accepts an EIP-712 signature bound to the exact trade, recipient, token, amount, nonce, chain, contract, and deadline.
      security: [{HmacAuth: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      x-required-scope: trades:release_intent
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ReleaseIntent'}
      responses:
        '202':
          description: Signed transaction intent submitted for execution
          content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '409': {$ref: '#/components/responses/Conflict'}
  /trades/{tradeId}/disputes:
    post:
      tags: [Disputes]
      operationId: openDispute
      summary: Open one preserved dispute for a trade
      description: Payment must be marked and the one-hour dispute cooldown must have elapsed.
      security: [{HmacAuth: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      x-required-scope: trades:dispute
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/OpenDisputeRequest'}
      responses:
        '201':
          description: Dispute created and trade frozen
          content: {application/json: {schema: {$ref: '#/components/schemas/Dispute'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /disputes/{disputeId}:
    get:
      tags: [Disputes]
      operationId: getParticipantDispute
      summary: Get a dispute visible to a trade participant
      security: [{HmacAuth: []}]
      x-required-scope: trades:read
      parameters: [{$ref: '#/components/parameters/DisputeId'}]
      responses:
        '200':
          description: Dispute
          content: {application/json: {schema: {$ref: '#/components/schemas/Dispute'}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /staff/auth/login:
    post:
      tags: [Staff]
      operationId: staffLogin
      summary: Start a restricted staff session
      x-internal: true
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/StaffLoginRequest'}
      responses:
        '200':
          description: Staff session created
          headers:
            Set-Cookie: {schema: {type: string}}
          content:
            application/json:
              schema:
                type: object
                required: [staff, expiresAt]
                properties:
                  staff: {$ref: '#/components/schemas/StaffIdentity'}
                  expiresAt: {type: string, format: date-time}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /staff/auth/me:
    get:
      tags: [Staff]
      operationId: getCurrentStaff
      summary: Get the authenticated staff identity
      x-internal: true
      security: [{StaffCookie: []}]
      responses:
        '200':
          description: Current staff identity
          content:
            application/json:
              schema:
                type: object
                required: [staff]
                properties:
                  staff: {$ref: '#/components/schemas/StaffIdentity'}
        '401': {$ref: '#/components/responses/Unauthorized'}
  /staff/auth/logout:
    post:
      tags: [Staff]
      operationId: staffLogout
      summary: Revoke the current staff session
      x-internal: true
      security: [{StaffCookie: []}]
      responses:
        '204': {description: Staff session revoked}
  /staff/moderators:
    get:
      tags: [Staff]
      operationId: listModerators
      summary: List named moderator accounts
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      responses:
        '200':
          description: Staff accounts
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/StaffIdentity'}}
        '403': {$ref: '#/components/responses/Forbidden'}
    post:
      tags: [Staff]
      operationId: createModerator
      summary: Create a named moderator account
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreateModeratorRequest'}
      responses:
        '201':
          description: Moderator created
          content: {application/json: {schema: {$ref: '#/components/schemas/StaffIdentity'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/moderators/{staffId}/pin:
    post:
      tags: [Staff]
      operationId: rotateModeratorPin
      summary: Rotate a moderator PIN and revoke existing sessions
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      parameters:
        - name: staffId
          in: path
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/RotateModeratorPinRequest'}
      responses:
        '200':
          description: Moderator PIN rotated
          content: {application/json: {schema: {$ref: '#/components/schemas/StaffIdentity'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
  /staff/moderators/{staffId}:
    delete:
      tags: [Staff]
      operationId: removeModerator
      summary: Remove a moderator and revoke existing sessions
      description: Suspends access, reopens assigned but undecided disputes, and preserves historical moderator attribution.
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      parameters:
        - name: staffId
          in: path
          required: true
          schema: {type: string}
      responses:
        '200':
          description: Moderator access removed
          content: {application/json: {schema: {$ref: '#/components/schemas/StaffIdentity'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
  /staff/payment-methods:
    get:
      tags: [Staff]
      operationId: listManagedPaymentMethods
      summary: List active and disabled payment methods
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      responses:
        '200':
          description: Managed payment methods
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/PaymentMethod'}}
        '403': {$ref: '#/components/responses/Forbidden'}
    post:
      tags: [Staff]
      operationId: createPaymentMethod
      summary: Add a payment method
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CreatePaymentMethodRequest'}
      responses:
        '201':
          description: Payment method created
          content: {application/json: {schema: {$ref: '#/components/schemas/PaymentMethod'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/payment-methods/{paymentMethodId}:
    patch:
      tags: [Staff]
      operationId: updatePaymentMethod
      summary: Update or disable a payment method
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      parameters:
        - name: paymentMethodId
          in: path
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/UpdatePaymentMethodRequest'}
      responses:
        '200':
          description: Payment method updated
          content: {application/json: {schema: {$ref: '#/components/schemas/PaymentMethod'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/disputes:
    get:
      tags: [Staff]
      operationId: listStaffDisputes
      summary: List the moderation dispute queue
      x-internal: true
      security: [{StaffCookie: []}]
      parameters:
        - name: status
          in: query
          schema: {type: string, enum: [OPEN, ASSIGNED, AWAITING_MULTISIG, RESOLVED]}
      responses:
        '200':
          description: Dispute queue
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/StaffDisputeView'}}
  /staff/disputes/{disputeId}:
    get:
      tags: [Staff]
      operationId: getStaffDispute
      summary: Get a moderation dispute case
      x-internal: true
      security: [{StaffCookie: []}]
      parameters: [{$ref: '#/components/parameters/DisputeId'}]
      responses:
        '200':
          description: Dispute case
          content: {application/json: {schema: {$ref: '#/components/schemas/StaffDisputeView'}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /staff/disputes/{disputeId}/assign:
    post:
      tags: [Staff]
      operationId: assignStaffDispute
      summary: Assign an open dispute to an active moderator
      description: Moderators may self-assign. Administrators may choose an active moderator.
      x-internal: true
      security: [{StaffCookie: []}]
      parameters: [{$ref: '#/components/parameters/DisputeId'}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/AssignDisputeRequest'}
      responses:
        '200':
          description: Assigned dispute
          content: {application/json: {schema: {$ref: '#/components/schemas/Dispute'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/disputes/{disputeId}/recommend:
    post:
      tags: [Staff]
      operationId: recommendDisputeOutcome
      summary: Submit an attributable settlement recommendation
      description: This freezes the case in AWAITING_MULTISIG. It cannot move escrow funds.
      x-internal: true
      x-required-role: MODERATOR
      security: [{StaffCookie: []}]
      parameters: [{$ref: '#/components/parameters/DisputeId'}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/RecommendDisputeRequest'}
      responses:
        '200':
          description: Recommendation recorded
          content: {application/json: {schema: {$ref: '#/components/schemas/Dispute'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/disputes/{disputeId}/settlement-action:
    get:
      tags: [Staff]
      operationId: prepareDisputeSettlement
      summary: Prepare executor calldata from the recorded recommendation
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      parameters: [{$ref: '#/components/parameters/DisputeId'}]
      responses:
        '200':
          description: Executor transaction payload
          content: {application/json: {schema: {$ref: '#/components/schemas/PreparedDisputeSettlement'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/disputes/{disputeId}/settlement-transactions:
    post:
      tags: [Staff]
      operationId: submitDisputeSettlementTransaction
      summary: Register an externally submitted executor transaction
      description: The staff session never signs or sends funds. The configured on-chain executor remains authoritative.
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      parameters: [{$ref: '#/components/parameters/DisputeId'}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [transactionHash]
              properties:
                transactionHash: {type: string, pattern: '^0x[a-fA-F0-9]{64}$'}
      responses:
        '202':
          description: Settlement transaction accepted for confirmation tracking
          content: {application/json: {schema: {$ref: '#/components/schemas/ChainTransaction'}}}
        '403': {$ref: '#/components/responses/Forbidden'}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/audit:
    get:
      tags: [Staff]
      operationId: listStaffAudit
      summary: List attributable staff audit events
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      parameters:
        - name: limit
          in: query
          schema: {type: integer, minimum: 1, maximum: 500, default: 100}
      responses:
        '200':
          description: Staff audit events
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/StaffAuditEvent'}}
        '403': {$ref: '#/components/responses/Forbidden'}
  /staff/evidence-access-audit:
    get:
      tags: [Staff]
      operationId: listEvidenceAccessAudit
      summary: List immutable evidence download audit records
      x-internal: true
      x-required-role: ADMIN
      security: [{StaffCookie: []}]
      parameters:
        - name: limit
          in: query
          schema: {type: integer, minimum: 1, maximum: 500, default: 100}
      responses:
        '200':
          description: Evidence access audit records
          content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/EvidenceAccessAudit'}}}}}}
        '403': {$ref: '#/components/responses/Forbidden'}
  /staff/evidence/{evidenceId}/content:
    get:
      tags: [Staff]
      operationId: downloadEvidenceForStaff
      summary: Download clean evidence as an administrator or assigned moderator
      x-internal: true
      security: [{StaffCookie: []}]
      parameters: [{$ref: '#/components/parameters/EvidenceId'}]
      responses:
        '200': {description: Decrypted evidence content, content: {application/octet-stream: {schema: {type: string, format: binary}}}}
        '404': {description: Evidence unavailable or not assigned to this moderator}
  /trades/{tradeId}/messages:
    get:
      tags: [Messages]
      operationId: listTradeMessages
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - name: afterSequence
          in: query
          schema: {type: integer, minimum: 0, default: 0}
        - name: limit
          in: query
          schema: {type: integer, minimum: 1, maximum: 200, default: 100}
      responses:
        '200':
          description: Authorized trade message page
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/Message'}}
                  nextAfterSequence: {type: integer}
    post:
      tags: [Messages]
      operationId: createTradeMessage
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [clientMessageId]
              properties:
                body: {type: string, maxLength: 4000, default: ''}
                clientMessageId: {type: string, format: uuid}
                evidenceIds: {type: array, maxItems: 5, items: {type: string, format: uuid}, default: []}
      responses:
        '201':
          description: Message created
          content: {application/json: {schema: {$ref: '#/components/schemas/Message'}}}
  /trades/{tradeId}/evidence:
    post:
      tags: [Messages]
      operationId: registerTradeEvidence
      summary: Register an evidence manifest before encrypted upload
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateEvidenceRequest'
      responses:
        '201':
          description: Evidence manifest awaiting content upload
          content: {application/json: {schema: {$ref: '#/components/schemas/Evidence'}}}
        '413':
          description: Evidence exceeds the 20 MB limit
          content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
  /evidence/{evidenceId}/content:
    get:
      tags: [Messages]
      operationId: downloadTradeEvidenceContent
      summary: Download clean evidence as a trade participant
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/EvidenceId'}
      responses:
        '200': {description: Decrypted evidence content, content: {application/octet-stream: {schema: {type: string, format: binary}}}}
        '404': {description: Evidence unavailable or not owned by the participant's trade}
    put:
      tags: [Messages]
      operationId: uploadTradeEvidenceContent
      summary: Upload evidence bytes for encryption and quarantine
      description: The server verifies the declared size and SHA-256 digest, encrypts the immutable payload at rest, and keeps it quarantined until a malware scanner marks it available.
      security: [{UserCookie: []}]
      parameters:
        - {$ref: '#/components/parameters/EvidenceId'}
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: {type: string, format: binary, maxLength: 20971520}
      responses:
        '200':
          description: Encrypted evidence is quarantined
          content: {application/json: {schema: {$ref: '#/components/schemas/Evidence'}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /developer/clients:
    get:
      tags: [Developer]
      operationId: listDeveloperClients
      summary: List developer apps and redacted credentials owned by the wallet
      security: [{UserCookie: []}]
      responses:
        '200':
          description: Developer apps
          content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/DeveloperClient'}}}}}}
    post:
      tags: [Developer]
      operationId: createDeveloperClient
      summary: Create an isolated sandbox developer app
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/CreateDeveloperClientRequest'}}}
      responses:
        '201': {description: Developer app created, content: {application/json: {schema: {$ref: '#/components/schemas/DeveloperClient'}}}}
        '403': {$ref: '#/components/responses/Forbidden'}
  /developer/clients/{clientId}/credentials:
    post:
      tags: [Developer]
      operationId: createApiCredential
      summary: Issue a scoped HMAC signing secret
      description: The plaintext signing secret is returned once and encrypted at rest.
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/ClientId'}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/CreateApiCredentialRequest'}}}
      responses:
        '201': {description: Credential and one-time secret, content: {application/json: {schema: {$ref: '#/components/schemas/IssuedApiCredential'}}}}
  /developer/credentials/{credentialId}:
    delete:
      tags: [Developer]
      operationId: revokeApiCredential
      summary: Revoke an API credential immediately
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/CredentialId'}]
      responses:
        '200': {description: Revoked credential, content: {application/json: {schema: {$ref: '#/components/schemas/ApiCredential'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /developer/request-log:
    get:
      tags: [Developer]
      operationId: listDeveloperRequestAudit
      summary: List authenticated automation requests for the wallet's apps
      security: [{UserCookie: []}]
      responses:
        '200': {description: Redacted request audit, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/ApiRequestAudit'}}}}}}}
  /developer/clients/{clientId}/webhooks:
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/ClientId'}]
      responses:
        '200': {description: Redacted endpoints, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/WebhookEndpoint'}}}}}}}
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Register an HTTPS webhook endpoint
      description: Local and private network targets are rejected. The signing secret is returned once.
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/ClientId'}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/CreateWebhookEndpointRequest'}}}
      responses:
        '201': {description: Endpoint and one-time signing secret, content: {application/json: {schema: {$ref: '#/components/schemas/IssuedWebhookEndpoint'}}}}
  /developer/clients/{clientId}/webhook-deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: Inspect delivery attempts and terminal failures
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/ClientId'}]
      responses:
        '200': {description: Recent deliveries, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/WebhookDeliveryView'}}}}}}}
  /developer/webhooks/{webhookId}:
    delete:
      tags: [Webhooks]
      operationId: disableWebhookEndpoint
      summary: Disable a webhook endpoint
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/WebhookId'}]
      responses:
        '200': {description: Disabled endpoint, content: {application/json: {schema: {$ref: '#/components/schemas/WebhookEndpoint'}}}}
  /developer/webhook-deliveries/{deliveryId}/replay:
    post:
      tags: [Webhooks]
      operationId: replayWebhookDelivery
      summary: Queue a new attempt for a completed or failed delivery
      security: [{UserCookie: []}]
      parameters: [{$ref: '#/components/parameters/DeliveryId'}]
      responses:
        '201': {description: Replay queued, content: {application/json: {schema: {$ref: '#/components/schemas/WebhookDelivery'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /automation/offers:
    get:
      tags: [Developer, Offers]
      operationId: listAutomationOffers
      summary: List offers owned by the signed developer app wallet
      security: [{HmacAuth: []}]
      x-required-scope: offers:read
      responses:
        '200': {description: Owned offers, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Offer'}}}}}}}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      tags: [Developer, Offers]
      operationId: createAutomationOffer
      summary: Create a paused offer for the signed developer app wallet
      security: [{HmacAuth: []}]
      x-required-scope: offers:write
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/CreateOfferRequest'}}}
      responses:
        '201': {description: Offer created, content: {application/json: {schema: {$ref: '#/components/schemas/Offer'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /automation/offers/{offerId}:
    patch:
      tags: [Developer, Offers]
      operationId: updateAutomationOffer
      summary: Update an offer owned by the signed developer app wallet
      security: [{HmacAuth: []}]
      x-required-scope: offers:write
      parameters:
        - {$ref: '#/components/parameters/OfferId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/UpdateOfferRequest'}}}
      responses:
        '200': {description: Offer updated, content: {application/json: {schema: {$ref: '#/components/schemas/Offer'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /automation/trade-quotes:
    post:
      tags: [Developer, Trades]
      operationId: createAutomationTradeQuote
      summary: Create immutable confirmation terms without opening a trade
      security: [{HmacAuth: []}]
      x-required-scope: trades:create
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/CreateTradeQuoteRequest'}}}
      responses:
        '201': {description: Short-lived quote, content: {application/json: {schema: {$ref: '#/components/schemas/TradeQuote'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /automation/trades:
    get:
      tags: [Developer, Trades]
      operationId: listAutomationTrades
      summary: List active or completed trades for the signed developer app wallet
      security: [{HmacAuth: []}]
      x-required-scope: trades:read
      parameters:
        - name: state
          in: query
          schema: {type: string, enum: [active, completed], default: active}
      responses:
        '200': {description: Trades, content: {application/json: {schema: {type: object, properties: {data: {type: array, items: {$ref: '#/components/schemas/Trade'}}}}}}}
        '401': {$ref: '#/components/responses/Unauthorized'}
    post:
      tags: [Developer, Trades]
      operationId: createAutomationTrade
      summary: Open a trade from a valid confirmation token
      security: [{HmacAuth: []}]
      x-required-scope: trades:create
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [confirmationToken]
              properties: {confirmationToken: {type: string, minLength: 30}}
      responses:
        '201': {description: Trade created, content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /automation/trades/{tradeId}:
    get:
      tags: [Developer, Trades]
      operationId: getAutomationTrade
      security: [{HmacAuth: []}]
      x-required-scope: trades:read
      parameters: [{$ref: '#/components/parameters/TradeId'}]
      responses:
        '200': {description: Trade, content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /automation/trades/{tradeId}/cancel:
    post:
      tags: [Developer, Trades]
      operationId: cancelAutomationTrade
      summary: Cancel as buyer before payment is marked
      security: [{HmacAuth: []}]
      x-required-scope: trades:create
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      responses:
        '200': {description: Trade cancelled, content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /automation/trades/{tradeId}/paid:
    post:
      tags: [Developer, Trades]
      operationId: markAutomationTradePaid
      summary: Mark fiat payment as sent without releasing escrow
      security: [{HmacAuth: []}]
      x-required-scope: trades:mark_paid
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      responses:
        '200': {description: Payment marked, content: {application/json: {schema: {$ref: '#/components/schemas/Trade'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /automation/trades/{tradeId}/disputes:
    post:
      tags: [Developer, Disputes]
      operationId: openAutomationDispute
      summary: Open a preserved dispute after the payment cooldown
      security: [{HmacAuth: []}]
      x-required-scope: trades:dispute
      parameters:
        - {$ref: '#/components/parameters/TradeId'}
        - {$ref: '#/components/parameters/IdempotencyKey'}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/OpenDisputeRequest'}}}
      responses:
        '201': {description: Dispute opened, content: {application/json: {schema: {$ref: '#/components/schemas/Dispute'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /gift-cards/verification-capabilities:
    get:
      tags: [Gift cards]
      operationId: getGiftCardVerificationCapabilities
      summary: Read the currently configured gift-card verification capabilities
      description: Reports capability availability only. A provider or model result never settles escrow; vendor redemption and BNB Chain lifecycle rules remain authoritative.
      responses:
        '200': {description: Verification capability status, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardVerificationCapabilities'}}}}
  /gift-cards/brands:
    get:
      tags: [Gift cards]
      operationId: listGiftCardBrands
      summary: List active gift-card brands and supported markets
      responses:
        '200': {description: Active brands, content: {application/json: {schema: {type: object, required: [data], properties: {data: {type: array, items: {$ref: '#/components/schemas/GiftCardBrand'}}}}}}}
  /gift-cards/offers:
    get:
      tags: [Gift cards]
      operationId: listGiftCardBuyOffers
      summary: List active vendor buy offers with current capacity
      parameters:
        - {name: brandId, in: query, schema: {type: string, format: uuid}}
        - {name: country, in: query, schema: {type: string, pattern: '^[A-Z]{2}$'}}
        - {name: currency, in: query, schema: {type: string, pattern: '^[A-Z]{3}$'}}
        - {name: cardType, in: query, schema: {type: string, enum: [PHYSICAL, E_CODE]}}
        - {name: settlementAsset, in: query, schema: {type: string, enum: [BNB, USDT]}}
      responses:
        '200': {description: Public offers, content: {application/json: {schema: {type: object, required: [data], properties: {data: {type: array, items: {$ref: '#/components/schemas/PublicGiftCardBuyOffer'}}}}}}}
  /gift-cards/offers/{offerId}:
    get:
      tags: [Gift cards]
      operationId: getGiftCardBuyOffer
      parameters:
        - {name: offerId, in: path, required: true, schema: {type: string, format: uuid}}
      responses:
        '200': {description: Public offer, content: {application/json: {schema: {$ref: '#/components/schemas/PublicGiftCardBuyOffer'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /gift-cards/matches:
    post:
      tags: [Gift cards]
      operationId: matchGiftCardBuyOffers
      summary: Rank eligible vendor buy offers for one card
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardMatchRequest'}}}
      responses:
        '200': {description: Eligible ranked offers, content: {application/json: {schema: {type: object, required: [data], properties: {data: {type: array, items: {$ref: '#/components/schemas/GiftCardMatch'}}}}}}}
        '401': {$ref: '#/components/responses/Unauthorized'}
  /gift-cards/quotes:
    post:
      tags: [Gift cards]
      operationId: createGiftCardQuote
      summary: Create an immutable short-lived seller quote
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardQuoteRequest'}}}
      responses:
        '201': {description: Signed quote, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardQuote'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /gift-cards/trades:
    get:
      tags: [Gift cards]
      operationId: listMyGiftCardTrades
      summary: List the connected user's gift-card trades
      security: [{UserCookie: []}]
      parameters:
        - {name: state, in: query, schema: {type: string, enum: [active, completed], default: active}}
      responses:
        '200': {description: Participant gift-card trades, content: {application/json: {schema: {type: object, required: [data], properties: {data: {type: array, items: {$ref: '#/components/schemas/GiftCardTrade'}}}}}}}
    post:
      tags: [Gift cards]
      operationId: openGiftCardTrade
      summary: Atomically reserve vendor liquidity and open the trade
      description: Card submission remains disabled until the PENDING_CHAIN reservation reaches finalized BNB state.
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [confirmationToken]
              properties: {confirmationToken: {type: string, minLength: 30, maxLength: 1000}}
      responses:
        '201': {description: Trade and pending reservation, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardTradeOpenResult'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /gift-cards/trades/{giftCardTradeId}:
    get:
      tags: [Gift cards]
      operationId: getGiftCardTrade
      security: [{UserCookie: []}]
      parameters:
        - {name: giftCardTradeId, in: path, required: true, schema: {type: string, format: uuid}}
      responses:
        '200': {description: Participant trade, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardTrade'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /gift-cards/vendor/apply:
    post:
      tags: [Gift cards]
      operationId: applyAsGiftCardVendor
      security: [{UserCookie: []}]
      responses:
        '201': {description: Existing or new vendor application, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardVendorProfile'}}}}
  /gift-cards/vendor/workspace:
    get:
      tags: [Gift cards]
      operationId: getGiftCardVendorWorkspace
      security: [{UserCookie: []}]
      responses:
        '200': {description: Vendor profile, liquidity, offers, and active count, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardVendorWorkspace'}}}}
        '404': {$ref: '#/components/responses/NotFound'}
  /gift-cards/vendor/offers:
    post:
      tags: [Gift cards]
      operationId: createGiftCardVendorOffer
      security: [{UserCookie: []}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardOfferWrite'}}}
      responses:
        '201': {description: Paused offer awaiting liquidity activation, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardBuyOffer'}}}}
        '403': {$ref: '#/components/responses/Forbidden'}
  /gift-cards/vendor/offers/{offerId}:
    patch:
      tags: [Gift cards]
      operationId: updateGiftCardVendorOffer
      security: [{UserCookie: []}]
      parameters:
        - {name: offerId, in: path, required: true, schema: {type: string, format: uuid}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardOfferUpdate'}}}
      responses:
        '200': {description: Updated offer, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardBuyOffer'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/gift-cards/brands:
    get:
      tags: [Staff, Gift cards]
      operationId: listStaffGiftCardBrands
      security: [{StaffCookie: []}]
      responses:
        '200': {description: All configured brands, content: {application/json: {schema: {type: object, required: [data], properties: {data: {type: array, items: {$ref: '#/components/schemas/GiftCardBrand'}}}}}}}
    post:
      tags: [Staff, Gift cards]
      operationId: createStaffGiftCardBrand
      security: [{StaffCookie: []}]
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardBrandWrite'}}}
      responses:
        '201': {description: Created brand, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardBrand'}}}}
  /staff/gift-cards/brands/{brandId}:
    patch:
      tags: [Staff, Gift cards]
      operationId: updateStaffGiftCardBrand
      security: [{StaffCookie: []}]
      parameters:
        - {name: brandId, in: path, required: true, schema: {type: string, format: uuid}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardBrandUpdate'}}}
      responses:
        '200': {description: Updated brand, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardBrand'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
  /staff/gift-cards/vendors:
    get:
      tags: [Staff, Gift cards]
      operationId: listStaffGiftCardVendors
      security: [{StaffCookie: []}]
      responses:
        '200': {description: Vendor applications, content: {application/json: {schema: {type: object, required: [data], properties: {data: {type: array, items: {$ref: '#/components/schemas/GiftCardVendorProfile'}}}}}}}
  /staff/gift-cards/vendors/{vendorUserId}:
    patch:
      tags: [Staff, Gift cards]
      operationId: reviewStaffGiftCardVendor
      security: [{StaffCookie: []}]
      parameters:
        - {name: vendorUserId, in: path, required: true, schema: {type: string, format: uuid}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardVendorReview'}}}
      responses:
        '200': {description: Reviewed vendor, content: {application/json: {schema: {$ref: '#/components/schemas/GiftCardVendorProfile'}}}}
        '409': {$ref: '#/components/responses/Conflict'}
components:
  securitySchemes:
    HmacAuth:
      type: apiKey
      in: header
      name: Vaypeer-Client-Id
      description: |
        Composite HMAC authentication. Requests also require Vaypeer-Key-Id,
        Vaypeer-Timestamp (Unix seconds), a unique Vaypeer-Nonce, and
        Vaypeer-Signature (`v1=` plus lowercase hex HMAC-SHA256). The signed input is
        METHOD, canonical sorted path/query, body SHA-256, timestamp, nonce, and client ID,
        separated by newlines. Clock skew is limited to five minutes and nonces are single-use.
    StaffCookie:
      type: apiKey
      in: cookie
      name: vaypeer_staff_session
      description: Signed, HTTP-only staff session cookie issued after username and PIN verification.
    UserCookie:
      type: apiKey
      in: cookie
      name: vaypeer_session
      description: Signed, HTTP-only wallet session cookie issued after SIWE verification.
  parameters:
    Asset: {name: asset, in: query, schema: {type: string, enum: [BNB, USDT]}}
    Fiat: {name: fiat, in: query, schema: {type: string, minLength: 3, maxLength: 3}}
    Cursor: {name: cursor, in: query, schema: {type: string}}
    OfferId: {name: offerId, in: path, required: true, schema: {type: string}}
    UserId: {name: userId, in: path, required: true, schema: {type: string, format: uuid}}
    TradeId: {name: tradeId, in: path, required: true, schema: {type: string}}
    DisputeId: {name: disputeId, in: path, required: true, schema: {type: string}}
    EvidenceId: {name: evidenceId, in: path, required: true, schema: {type: string, format: uuid}}
    ClientId: {name: clientId, in: path, required: true, schema: {type: string, format: uuid}}
    CredentialId: {name: credentialId, in: path, required: true, schema: {type: string, format: uuid}}
    WebhookId: {name: webhookId, in: path, required: true, schema: {type: string, format: uuid}}
    DeliveryId: {name: deliveryId, in: path, required: true, schema: {type: string, format: uuid}}
    IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 16, maxLength: 128}}
  responses:
    Unauthorized:
      description: Invalid authentication, signature, scope, nonce, or timestamp
      content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
    Conflict:
      description: State, idempotency, liquidity, or transition conflict
      content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
    NotFound:
      description: Resource not found
      content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
    Forbidden:
      description: Authenticated principal lacks the required role or assignment
      content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
    TooManyRequests:
      description: Rate limit or staff account lockout is active
      content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
    ServiceUnavailable:
      description: The configured BNB Chain reconciliation service is unavailable
      content: {application/problem+json: {schema: {$ref: '#/components/schemas/Problem'}}}
  schemas:
    GiftCardBrandWrite:
      type: object
      additionalProperties: false
      required: [slug, name, supportedCountries, supportedCurrencies, supportedCardTypes, supportedDenominations, requiresReceipt, protectionLevel, riskWeight, status]
      properties:
        slug: {type: string, pattern: '^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$'}
        name: {type: string, minLength: 2, maxLength: 80}
        supportedCountries: {type: array, minItems: 1, maxItems: 250, items: {type: string, pattern: '^[A-Z]{2}$'}}
        supportedCurrencies: {type: array, minItems: 1, maxItems: 200, items: {type: string, pattern: '^[A-Z]{3}$'}}
        supportedCardTypes: {type: array, minItems: 1, maxItems: 2, items: {type: string, enum: [PHYSICAL, E_CODE]}}
        supportedDenominations: {type: object, additionalProperties: {type: array, maxItems: 100, items: {type: string}}}
        requiresReceipt: {type: boolean}
        protectionLevel: {type: string, enum: [A_PLUS, A, B]}
        riskWeight: {type: number, minimum: 0, maximum: 10}
        status: {type: string, enum: [ACTIVE, DISABLED]}
    GiftCardVerificationCapabilities:
      type: object
      additionalProperties: false
      required: [mode, provider, operationalStatus, capabilities, automaticPassEligible]
      properties:
        mode: {type: string, enum: [rules-only, remote]}
        provider: {type: string}
        operationalStatus: {type: string, enum: [MANUAL_REVIEW, CONFIGURED]}
        capabilities:
          type: object
          additionalProperties: false
          required: [ocr, issuerLookup, balanceCheck, tamperAnalysis, duplicateCheck]
          properties:
            ocr: {type: boolean}
            issuerLookup: {type: boolean}
            balanceCheck: {type: boolean}
            tamperAnalysis: {type: boolean}
            duplicateCheck: {type: boolean}
        automaticPassEligible: {type: boolean}
    GiftCardBrand:
      allOf:
        - {$ref: '#/components/schemas/GiftCardBrandWrite'}
        - type: object
          required: [id, version, createdByStaffId, createdAt, updatedAt]
          properties:
            id: {type: string, format: uuid}
            version: {type: integer, minimum: 1}
            createdByStaffId: {type: string, format: uuid}
            createdAt: {type: string, format: date-time}
            updatedAt: {type: string, format: date-time}
    GiftCardBrandUpdate:
      type: object
      additionalProperties: false
      required: [expectedVersion]
      properties:
        expectedVersion: {type: integer, minimum: 1}
        slug: {type: string, pattern: '^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$'}
        name: {type: string, minLength: 2, maxLength: 80}
        supportedCountries: {type: array, minItems: 1, maxItems: 250, items: {type: string, pattern: '^[A-Z]{2}$'}}
        supportedCurrencies: {type: array, minItems: 1, maxItems: 200, items: {type: string, pattern: '^[A-Z]{3}$'}}
        supportedCardTypes: {type: array, minItems: 1, maxItems: 2, items: {type: string, enum: [PHYSICAL, E_CODE]}}
        supportedDenominations: {type: object, additionalProperties: {type: array, maxItems: 100, items: {type: string}}}
        requiresReceipt: {type: boolean}
        protectionLevel: {type: string, enum: [A_PLUS, A, B]}
        riskWeight: {type: number, minimum: 0, maximum: 10}
        status: {type: string, enum: [ACTIVE, DISABLED]}
    GiftCardVendorProfile:
      type: object
      required: [userId, status, kycStatus, riskTier, trustScore, brandPermissions, dailyTradeLimit, maxConcurrentTrades, version, createdAt, updatedAt]
      properties:
        userId: {type: string, format: uuid}
        status: {type: string, enum: [PENDING, ACTIVE, LIMITED, SUSPENDED, FROZEN, REJECTED]}
        kycStatus: {type: string, enum: [NOT_STARTED, PENDING, VERIFIED, REJECTED]}
        riskTier: {type: string, enum: [LOW, STANDARD, HIGH]}
        trustScore: {type: number, minimum: 0, maximum: 100}
        brandPermissions: {type: array, items: {type: string, format: uuid}}
        dailyTradeLimit: {type: string}
        maxConcurrentTrades: {type: integer, minimum: 1, maximum: 100}
        reviewedByStaffId: {type: [string, 'null'], format: uuid}
        version: {type: integer, minimum: 1}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    GiftCardVendorReview:
      type: object
      additionalProperties: false
      required: [expectedVersion]
      properties:
        expectedVersion: {type: integer, minimum: 1}
        status: {type: string, enum: [PENDING, ACTIVE, LIMITED, SUSPENDED, FROZEN, REJECTED]}
        kycStatus: {type: string, enum: [NOT_STARTED, PENDING, VERIFIED, REJECTED]}
        riskTier: {type: string, enum: [LOW, STANDARD, HIGH]}
        trustScore: {type: number, minimum: 0, maximum: 100}
        brandPermissions: {type: array, maxItems: 500, items: {type: string, format: uuid}}
        dailyTradeLimit: {type: string}
        maxConcurrentTrades: {type: integer, minimum: 1, maximum: 100}
    GiftCardOfferWrite:
      type: object
      additionalProperties: false
      required: [brandId, country, currency, cardType, minimumCardValue, maximumCardValue, rate, receiptRequired, receiptRequirements, dailyCapacity, totalLiquidityLimit, minimumSellerTrustLevel, maxConcurrentTrades, supportedDenominations, settlementAsset, settlementWindowSeconds]
      properties:
        brandId: {type: string, format: uuid}
        country: {type: string, pattern: '^[A-Z]{2}$'}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        cardType: {type: string, enum: [PHYSICAL, E_CODE]}
        minimumCardValue: {type: string}
        maximumCardValue: {type: string}
        rate: {type: string, description: Settlement asset per one unit of card face currency.}
        receiptRequired: {type: boolean}
        receiptRequirements: {type: array, maxItems: 20, items: {type: string, maxLength: 160}}
        dailyCapacity: {type: string}
        totalLiquidityLimit: {type: string}
        minimumSellerTrustLevel: {type: number, minimum: 0, maximum: 100}
        maxConcurrentTrades: {type: integer, minimum: 1, maximum: 100}
        supportedDenominations: {type: array, maxItems: 100, items: {type: string}}
        settlementAsset: {type: string, enum: [BNB, USDT]}
        settlementWindowSeconds: {type: integer, minimum: 60, maximum: 86400}
        autoRepricingEnabled: {type: boolean, default: false}
    GiftCardOfferUpdate:
      type: object
      additionalProperties: false
      required: [expectedVersion]
      properties:
        expectedVersion: {type: integer, minimum: 1}
        country: {type: string, pattern: '^[A-Z]{2}$'}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        cardType: {type: string, enum: [PHYSICAL, E_CODE]}
        minimumCardValue: {type: string}
        maximumCardValue: {type: string}
        rate: {type: string}
        receiptRequired: {type: boolean}
        receiptRequirements: {type: array, maxItems: 20, items: {type: string, maxLength: 160}}
        dailyCapacity: {type: string}
        totalLiquidityLimit: {type: string}
        minimumSellerTrustLevel: {type: number, minimum: 0, maximum: 100}
        maxConcurrentTrades: {type: integer, minimum: 1, maximum: 100}
        supportedDenominations: {type: array, maxItems: 100, items: {type: string}}
        settlementAsset: {type: string, enum: [BNB, USDT]}
        settlementWindowSeconds: {type: integer, minimum: 60, maximum: 86400}
        autoRepricingEnabled: {type: boolean}
        status: {type: string, enum: [ACTIVE, PAUSED, DISABLED]}
    GiftCardBuyOffer:
      allOf:
        - {$ref: '#/components/schemas/GiftCardOfferWrite'}
        - type: object
          required: [id, reference, vendorUserId, usedDailyCapacity, capacityResetAt, status, version, createdAt, updatedAt]
          properties:
            id: {type: string, format: uuid}
            reference: {type: string, pattern: '^\d+$'}
            vendorUserId: {type: string, format: uuid}
            usedDailyCapacity: {type: string}
            capacityResetAt: {type: string, format: date-time}
            status: {type: string, enum: [ACTIVE, PAUSED, DISABLED]}
            version: {type: integer, minimum: 1}
            createdAt: {type: string, format: date-time}
            updatedAt: {type: string, format: date-time}
    PublicGiftCardBuyOffer:
      allOf:
        - {$ref: '#/components/schemas/GiftCardBuyOffer'}
        - type: object
          required: [brand, vendor, availability]
          properties:
            brand:
              type: object
              required: [id, slug, name, protectionLevel]
              properties:
                id: {type: string, format: uuid}
                slug: {type: string}
                name: {type: string}
                protectionLevel: {type: string, enum: [A_PLUS, A, B]}
            vendor:
              type: object
              required: [profileId, verified, trustScore, completedTrades, completionRate, presence]
              properties:
                profileId: {type: string, format: uuid}
                username: {type: [string, 'null']}
                profilePhotoUrl: {type: [string, 'null']}
                verified: {type: boolean}
                trustScore: {type: number}
                completedTrades: {type: integer}
                completionRate: {type: number}
                presence: {type: string, enum: [ACTIVE, AWAY, OFFLINE]}
            availability:
              type: object
              required: [settlementAsset, availableLiquidity, remainingDailyCapacity, activeTrades]
              properties:
                settlementAsset: {type: string, enum: [BNB, USDT]}
                availableLiquidity: {type: string}
                remainingDailyCapacity: {type: string}
                activeTrades: {type: integer, minimum: 0}
    GiftCardMatchRequest:
      type: object
      additionalProperties: false
      required: [brandId, country, currency, cardType, faceValue, hasReceipt]
      properties:
        brandId: {type: string, format: uuid}
        country: {type: string, pattern: '^[A-Z]{2}$'}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        cardType: {type: string, enum: [PHYSICAL, E_CODE]}
        faceValue: {type: string}
        hasReceipt: {type: boolean}
        settlementAsset: {type: string, enum: [BNB, USDT]}
        sort: {type: string, enum: [RECOMMENDED, BEST_PAYOUT, FASTEST, SAFEST], default: RECOMMENDED}
    GiftCardMatch:
      allOf:
        - {$ref: '#/components/schemas/PublicGiftCardBuyOffer'}
        - type: object
          required: [quote, recommended]
          properties:
            quote:
              type: object
              required: [faceValue, sellerPayoutAmount]
              properties:
                faceValue: {type: string}
                sellerPayoutAmount: {type: string}
            recommended: {type: boolean}
    GiftCardQuoteRequest:
      type: object
      additionalProperties: false
      required: [offerId, brandId, country, currency, cardType, faceValue, hasReceipt]
      properties:
        offerId: {type: string, format: uuid}
        brandId: {type: string, format: uuid}
        country: {type: string, pattern: '^[A-Z]{2}$'}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        cardType: {type: string, enum: [PHYSICAL, E_CODE]}
        faceValue: {type: string}
        hasReceipt: {type: boolean}
        settlementAsset: {type: string, enum: [BNB, USDT]}
    GiftCardQuote:
      type: object
      required: [id, offerId, offerVersion, sellerUserId, vendorUserId, brandId, faceValue, rate, settlementAsset, sellerPayoutAmount, vendorCommissionBps, vendorCommissionAmount, totalReservationAmount, termsHash, expiresAt, createdAt, confirmationToken]
      properties:
        id: {type: string, format: uuid}
        offerId: {type: string, format: uuid}
        offerVersion: {type: integer, minimum: 1}
        sellerUserId: {type: string, format: uuid}
        vendorUserId: {type: string, format: uuid}
        brandId: {type: string, format: uuid}
        country: {type: string}
        currency: {type: string}
        cardType: {type: string, enum: [PHYSICAL, E_CODE]}
        faceValue: {type: string}
        hasReceipt: {type: boolean}
        rate: {type: string}
        settlementAsset: {type: string, enum: [BNB, USDT]}
        sellerPayoutAmount: {type: string}
        vendorCommissionBps: {type: integer, const: 100}
        vendorCommissionAmount: {type: string}
        totalReservationAmount: {type: string}
        termsHash: {type: string, pattern: '^[a-f0-9]{64}$'}
        expiresAt: {type: string, format: date-time}
        createdAt: {type: string, format: date-time}
        confirmationToken: {type: string, writeOnly: true}
    GiftCardTrade:
      type: object
      required: [id, reference, quoteId, offerId, vendorUserId, sellerUserId, brandId, faceValue, rate, settlementAsset, sellerPayoutAmount, vendorCommissionBps, vendorCommissionAmount, totalReservationAmount, termsHash, state, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        reference: {type: string, pattern: '^\d+$'}
        quoteId: {type: string, format: uuid}
        offerId: {type: string, format: uuid}
        vendorUserId: {type: string, format: uuid}
        sellerUserId: {type: string, format: uuid}
        brandId: {type: string, format: uuid}
        country: {type: string}
        currency: {type: string}
        cardType: {type: string, enum: [PHYSICAL, E_CODE]}
        faceValue: {type: string}
        rate: {type: string}
        settlementAsset: {type: string, enum: [BNB, USDT]}
        sellerPayoutAmount: {type: string}
        vendorCommissionBps: {type: integer, const: 100}
        vendorCommissionAmount: {type: string}
        totalReservationAmount: {type: string}
        termsHash: {type: string, pattern: '^[a-f0-9]{64}$'}
        state: {type: string, enum: [AWAITING_ESCROW_FINALITY, EVIDENCE_PENDING, UNDER_REVIEW, REVEAL_AUTHORIZED, REDEEMING, DISPUTED, RELEASED, REFUNDED, CANCELLED, EXPIRED]}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    GiftCardLiquidityReservation:
      type: object
      required: [id, quoteId, tradeId, offerId, liquidityAccountId, vendorUserId, sellerUserId, asset, amount, status, expiresAt, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        quoteId: {type: string, format: uuid}
        tradeId: {type: string, format: uuid}
        offerId: {type: string, format: uuid}
        liquidityAccountId: {type: string, format: uuid}
        vendorUserId: {type: string, format: uuid}
        sellerUserId: {type: string, format: uuid}
        asset: {type: string, enum: [BNB, USDT]}
        amount: {type: string}
        status: {type: string, enum: [PENDING_CHAIN, ACTIVE, RELEASED, EXPIRED, CANCELLED]}
        expiresAt: {type: string, format: date-time}
        finalizedAt: {type: [string, 'null'], format: date-time}
        releasedAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    GiftCardTradeOpenResult:
      type: object
      additionalProperties: false
      required: [trade, reservation, cardSubmissionAllowed]
      properties:
        trade: {$ref: '#/components/schemas/GiftCardTrade'}
        reservation: {$ref: '#/components/schemas/GiftCardLiquidityReservation'}
        cardSubmissionAllowed: {type: boolean, const: false}
    GiftCardVendorLiquidity:
      type: object
      required: [id, vendorUserId, chainId, asset, confirmedBalance, reservedBalance, version, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        vendorUserId: {type: string, format: uuid}
        chainId: {type: integer, enum: [56, 97]}
        asset: {type: string, enum: [BNB, USDT]}
        confirmedBalance: {type: string}
        reservedBalance: {type: string}
        version: {type: integer, minimum: 1}
        reconciledBlockNumber: {type: [integer, 'null']}
        reconciledAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    GiftCardVendorWorkspace:
      type: object
      required: [profile, liquidity, offers, activeTrades]
      properties:
        profile: {$ref: '#/components/schemas/GiftCardVendorProfile'}
        liquidity: {type: array, items: {$ref: '#/components/schemas/GiftCardVendorLiquidity'}}
        offers: {type: array, items: {$ref: '#/components/schemas/GiftCardBuyOffer'}}
        activeTrades: {type: integer, minimum: 0}
    ParticipantEvent:
      type: object
      required: [id, sequence, type, tradeId, tradeReference, payload, createdAt]
      properties:
        id: {type: string, format: uuid}
        sequence: {type: integer, minimum: 1}
        type: {type: string, enum: [TRADE_OPENED, TRADE_STATE_CHANGED, TRADE_MESSAGE, TRADE_EXPIRING, DISPUTE_OPENED, DISPUTE_UPDATED]}
        tradeId: {type: string, format: uuid}
        tradeReference: {type: string, pattern: '^\d+$'}
        payload: {type: object, additionalProperties: true}
        createdAt: {type: string, format: date-time}
    PushSubscriptionRequest:
      type: object
      additionalProperties: false
      required: [endpoint, keys]
      properties:
        endpoint: {type: string, format: uri, maxLength: 4096, pattern: '^https://'}
        expirationTime: {type: [integer, 'null'], minimum: 1}
        keys:
          type: object
          additionalProperties: false
          required: [p256dh, auth]
          properties:
            p256dh: {type: string, minLength: 40, maxLength: 256}
            auth: {type: string, minLength: 8, maxLength: 128}
    ParticipantProfile:
      type: object
      required: [id, walletAddress, status, verified, username, profilePhotoUrl, usernameChangedAt, nextUsernameChangeAt, profileUpdatedAt, createdAt]
      properties:
        id: {type: string, format: uuid}
        walletAddress: {type: string, pattern: '^0x[a-fA-F0-9]{40}$'}
        status: {type: string, enum: [ACTIVE, SUSPENDED, CLOSED]}
        verified: {type: boolean}
        username: {type: [string, 'null'], pattern: '^[A-Za-z0-9_]{3,24}$'}
        profilePhotoUrl: {type: [string, 'null']}
        usernameChangedAt: {type: [string, 'null'], format: date-time}
        nextUsernameChangeAt: {type: [string, 'null'], format: date-time}
        profileUpdatedAt: {type: string, format: date-time}
        createdAt: {type: string, format: date-time}
    PublicVendor:
      type: object
      required: [profileId, walletAddress, username, profilePhotoUrl, verified, completedTrades, completionRate, presence]
      properties:
        profileId: {type: string, format: uuid}
        walletAddress: {type: string, pattern: '^0x[a-fA-F0-9]{40}$'}
        username: {type: [string, 'null']}
        profilePhotoUrl: {type: [string, 'null']}
        verified: {type: boolean}
        completedTrades: {type: integer, minimum: 0}
        completionRate: {type: number, minimum: 0, maximum: 100}
        presence: {type: string, enum: [ACTIVE, AWAY, OFFLINE], description: Coarse activity state. Exact activity timestamps are private.}
    TraderBlockState:
      type: object
      required: [blocked]
      properties:
        blocked: {type: boolean}
    PublicFeedbackAuthor:
      type: object
      required: [profileId, username, walletAddress, profilePhotoUrl, verified]
      properties:
        profileId: {type: string, format: uuid}
        username: {type: [string, 'null']}
        walletAddress: {type: string, pattern: '^0x[a-fA-F0-9]{40}$'}
        profilePhotoUrl: {type: [string, 'null']}
        verified: {type: boolean}
    PublicTradeFeedback:
      type: object
      required: [id, tradeReference, rating, comment, createdAt, author]
      properties:
        id: {type: string, format: uuid}
        tradeReference: {type: string, pattern: '^\d+$'}
        rating: {type: string, enum: [POSITIVE, NEGATIVE]}
        comment: {type: string, maxLength: 500}
        createdAt: {type: string, format: date-time}
        author: {$ref: '#/components/schemas/PublicFeedbackAuthor'}
    PublicTraderProfile:
      type: object
      required: [id, username, walletAddress, profilePhotoUrl, verified, presence, memberSince, reputation, blocks, feedbackSummary, feedback, activeOffers]
      properties:
        id: {type: string, format: uuid}
        username: {type: [string, 'null']}
        walletAddress: {type: string, pattern: '^0x[a-fA-F0-9]{40}$'}
        profilePhotoUrl: {type: [string, 'null']}
        verified: {type: boolean}
        presence: {type: string, enum: [ACTIVE, AWAY, OFFLINE]}
        memberSince: {type: string, format: date-time}
        reputation:
          type: object
          required: [userId, completedTrades, completionRate, openDisputes]
          properties:
            userId: {type: string, format: uuid}
            completedTrades: {type: integer, minimum: 0}
            completionRate: {type: number, minimum: 0, maximum: 100}
            openDisputes: {type: integer, minimum: 0}
        blocks:
          type: object
          required: [blockedByCount, blockedCount]
          properties:
            blockedByCount: {type: integer, minimum: 0}
            blockedCount: {type: integer, minimum: 0}
        feedbackSummary:
          type: object
          required: [positive, negative, total]
          properties:
            positive: {type: integer, minimum: 0}
            negative: {type: integer, minimum: 0}
            total: {type: integer, minimum: 0}
        feedback: {type: array, items: {$ref: '#/components/schemas/PublicTradeFeedback'}}
        activeOffers: {type: array, items: {$ref: '#/components/schemas/Offer'}}
    DeveloperClient:
      type: object
      required: [id, ownerUserId, name, environment, status, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        ownerUserId: {type: string, format: uuid}
        name: {type: string}
        environment: {type: string, enum: [SANDBOX, LIVE]}
        status: {type: string, enum: [ACTIVE, SUSPENDED]}
        credentials: {type: array, items: {$ref: '#/components/schemas/ApiCredential'}}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    CreateDeveloperClientRequest:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name: {type: string, minLength: 2, maxLength: 80}
        environment: {type: string, enum: [SANDBOX, LIVE], default: SANDBOX, description: LIVE requires platform review and is rejected by self-service issuance.}
    ApiScope:
      type: string
      enum: [markets:read, offers:read, offers:write, trades:read, trades:create, trades:mark_paid, trades:dispute, messages:read, messages:write, webhooks:manage]
    ApiCredential:
      type: object
      required: [id, clientId, prefix, scopes, status, createdAt]
      properties:
        id: {type: string, format: uuid}
        clientId: {type: string, format: uuid}
        prefix: {type: string, pattern: '^vpk_(test|live)_'}
        scopes: {type: array, items: {$ref: '#/components/schemas/ApiScope'}}
        status: {type: string, enum: [ACTIVE, REVOKED]}
        expiresAt: {type: [string, 'null'], format: date-time}
        lastUsedAt: {type: [string, 'null'], format: date-time}
        revokedAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
    CreateApiCredentialRequest:
      type: object
      additionalProperties: false
      required: [scopes]
      properties:
        scopes: {type: array, minItems: 1, uniqueItems: true, items: {$ref: '#/components/schemas/ApiScope'}}
        expiresAt: {type: string, format: date-time, description: Maximum one year from issuance.}
    IssuedApiCredential:
      type: object
      required: [credential, signingSecret]
      properties:
        credential: {$ref: '#/components/schemas/ApiCredential'}
        signingSecret: {type: string, writeOnly: true, description: Returned once. Store it in a secrets manager.}
    ApiRequestAudit:
      type: object
      required: [id, clientId, credentialId, requestId, method, path, responseStatus, ipHash, createdAt]
      properties:
        id: {type: string, format: uuid}
        clientId: {type: string, format: uuid}
        credentialId: {type: string, format: uuid}
        requestId: {type: string}
        method: {type: string}
        path: {type: string}
        responseStatus: {type: integer}
        ipHash: {type: string, description: Privacy-preserving hash; raw addresses are not returned.}
        createdAt: {type: string, format: date-time}
    WebhookEndpoint:
      type: object
      required: [id, clientId, url, description, status, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        clientId: {type: string, format: uuid}
        url: {type: string, format: uri, pattern: '^https://'}
        description: {type: string, maxLength: 160}
        status: {type: string, enum: [ACTIVE, DISABLED]}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    CreateWebhookEndpointRequest:
      type: object
      additionalProperties: false
      required: [url]
      properties:
        url: {type: string, format: uri, pattern: '^https://', maxLength: 2048}
        description: {type: string, maxLength: 160, default: ''}
    IssuedWebhookEndpoint:
      type: object
      required: [endpoint, signingSecret]
      properties:
        endpoint: {$ref: '#/components/schemas/WebhookEndpoint'}
        signingSecret: {type: string, writeOnly: true, description: Returned once.}
    WebhookDelivery:
      type: object
      required: [id, eventId, endpointId, replayOfDeliveryId, attempt, status, nextAttemptAt, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        eventId: {type: string, format: uuid}
        endpointId: {type: string, format: uuid}
        replayOfDeliveryId: {type: [string, 'null'], format: uuid}
        attempt: {type: integer, minimum: 0, maximum: 6}
        status: {type: string, enum: [PENDING, DELIVERING, DELIVERED, FAILED]}
        responseCode: {type: [integer, 'null']}
        lastError: {type: [string, 'null']}
        nextAttemptAt: {type: string, format: date-time}
        deliveredAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    WebhookDeliveryView:
      type: object
      required: [delivery, eventType, endpointUrl]
      properties:
        delivery: {$ref: '#/components/schemas/WebhookDelivery'}
        eventType: {type: string}
        endpointUrl: {type: string, format: uri}
    Market:
      type: object
      required: [asset, chainId, fiat, paymentMethods, status]
      properties:
        asset: {type: string}
        chainId: {type: integer, enum: [56, 97]}
        fiat: {type: string}
        paymentMethods: {type: array, items: {type: string}}
        status: {type: string, enum: [ACTIVE, READ_ONLY, PAUSED]}
    MarketReferencePrice:
      type: object
      required: [asset, fiat, referencePrice, cryptoSource, fiatSource, fetchedAt, stale]
      properties:
        asset: {type: string, enum: [BNB, USDT]}
        fiat: {type: string}
        referencePrice: {type: string}
        cryptoSource: {type: string, enum: [BYBIT]}
        fiatSource: {type: string, enum: [FRANKFURTER, USD]}
        fetchedAt: {type: string, format: date-time}
        stale: {type: boolean}
    ParticipantBankAccount:
      type: object
      required: [id, paymentMethodId, country, currency, bankName, accountNumberLast4, isDefault, status, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        paymentMethodId: {type: string}
        country: {type: string, pattern: '^[A-Z]{2}$'}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        bankName: {type: string}
        accountNumberLast4: {type: string, description: Masked owner view; the full account number is never returned here.}
        isDefault: {type: boolean}
        status: {type: string, enum: [ACTIVE, ARCHIVED]}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    CreateParticipantBankAccount:
      type: object
      additionalProperties: false
      required: [paymentMethodId, country, currency, bankName, accountName, accountNumber]
      properties:
        paymentMethodId: {type: string}
        country: {type: string, pattern: '^[A-Z]{2}$'}
        currency: {type: string, pattern: '^[A-Z]{3}$'}
        bankName: {type: string, minLength: 2, maxLength: 120}
        accountName: {type: string, minLength: 2, maxLength: 120}
        accountNumber: {type: string, minLength: 3, maxLength: 64}
        isDefault: {type: boolean, default: false}
    Offer:
      type: object
      required: [id, reference, side, asset, chainId, fiat, fixedRate, marginPercent, availableCrypto, paymentMethodIds, status]
      properties:
        id: {type: string}
        reference: {type: string, pattern: '^\d+$'}
        side: {type: string, enum: [BUY, SELL]}
        asset: {type: string}
        chainId: {type: integer, enum: [56, 97]}
        fiat: {type: string}
        fixedRate: {type: string}
        marginPercent: {type: string, pattern: '^-?\d+(\.\d+)?$', description: Percentage applied to the market reference to produce fixedRate.}
        minimumFiat: {type: string}
        maximumFiat: {type: string}
        availableCrypto: {type: string}
        paymentMethodIds: {type: array, items: {type: string}}
        bankAccountRequired: {type: boolean}
        paymentWindowSeconds: {type: integer}
        policy: {$ref: '#/components/schemas/OfferPolicy'}
        customTerms: {type: string}
        status: {type: string, enum: [ACTIVE, PAUSED, OUT_OF_LIQUIDITY, HIDDEN, CLOSED]}
        vendor: {$ref: '#/components/schemas/PublicVendor'}
    OfferPolicy:
      type: object
      additionalProperties: false
      properties:
        minimumCompletedTrades: {type: integer, minimum: 0, maximum: 100000}
        minimumCompletionRate: {type: integer, minimum: 0, maximum: 100}
        verifiedUsersOnly: {type: boolean}
        blockVpn: {type: boolean}
        thirdPartyPayments: {type: string, enum: [NOT_ACCEPTED, ACCEPTED]}
        receiptRequired: {type: boolean}
    CreateOfferRequest:
      type: object
      additionalProperties: false
      required: [side, asset, chainId, fiat, fixedRate, minimumFiat, maximumFiat, paymentMethodIds, paymentWindowSeconds, policy]
      properties:
        side: {type: string, enum: [BUY, SELL]}
        asset: {type: string}
        chainId: {type: integer, enum: [56, 97]}
        fiat: {type: string}
        fixedRate: {type: string, pattern: '^\d+(\.\d+)?$'}
        marginPercent: {type: string, pattern: '^-?\d+(\.\d+)?$', default: '0', description: Percentage applied to the live market reference; the API recomputes fixedRate.}
        minimumFiat: {type: string}
        maximumFiat: {type: string}
        paymentMethodIds: {type: array, minItems: 1, items: {type: string}}
        bankAccountRequired: {type: boolean, default: false}
        payoutBankAccountId: {type: [string, 'null'], format: uuid, description: Private field for advertiser sell offers; never returned publicly.}
        paymentWindowSeconds: {type: integer, minimum: 300, maximum: 10800}
        policy: {$ref: '#/components/schemas/OfferPolicy'}
        customTerms: {type: string, maxLength: 5000, default: ''}
    UpdateOfferRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        side: {type: string, enum: [BUY, SELL]}
        asset: {type: string, enum: [BNB, USDT]}
        chainId: {type: integer, enum: [56, 97], default: 97}
        fiat: {type: string, pattern: '^[A-Za-z]{3}$'}
        fixedRate: {type: string, pattern: '^\d+(\.\d+)?$'}
        marginPercent: {type: string, pattern: '^-?\d+(\.\d+)?$'}
        minimumFiat: {type: string}
        maximumFiat: {type: string}
        paymentMethodIds: {type: array, minItems: 1, maxItems: 20, items: {type: string}}
        bankAccountRequired: {type: boolean}
        payoutBankAccountId: {type: [string, 'null'], format: uuid}
        paymentWindowSeconds: {type: integer, minimum: 300, maximum: 10800}
        policy: {$ref: '#/components/schemas/OfferPolicy'}
        customTerms: {type: string, maxLength: 5000}
        status: {type: string, enum: [ACTIVE, PAUSED, CLOSED]}
    Trade:
      type: object
      required: [id, reference, offerId, state, asset, grossCryptoAmount, buyerFeeBps, buyerFeeAmount, buyerNetAmount, sellerFeeAmount, fiat, fiatAmount]
      properties:
        id: {type: string}
        reference: {type: string, pattern: '^\d+$'}
        offerId: {type: string}
        state: {type: string, enum: [CREATED, RESERVED, PAYMENT_PENDING, PAYMENT_MARKED, DISPUTED, RELEASED, REFUNDED, CANCELLED, EXPIRED]}
        asset: {type: string}
        grossCryptoAmount: {type: string}
        buyerFeeBps: {type: integer, enum: [0, 100], description: 100 only when the offer-owning vendor is the crypto buyer.}
        buyerFeeAmount: {type: string, description: Vendor-buyer commission; zero for customer Buy-side offers.}
        buyerNetAmount: {type: string, description: Crypto received by the buyer after any vendor-buyer commission.}
        sellerFeeAmount: {type: string, description: Always zero}
        fiat: {type: string}
        fiatAmount: {type: string}
        paymentMarkedAt: {type: [string, 'null'], format: date-time}
        disputeAvailableAt: {type: [string, 'null'], format: date-time}
    ChainStatus:
      type: object
      required: [enabled, chainId, requiredConfirmations, escrowAddress]
      properties:
        enabled: {type: boolean}
        chainId: {type: integer, enum: [56, 97]}
        requiredConfirmations: {type: integer, minimum: 1}
        escrowAddress: {type: [string, 'null'], pattern: '^0x[a-fA-F0-9]{40}$'}
    ChainTransaction:
      type: object
      required: [id, purpose, transactionHash, status, confirmations, requiredConfirmations, createdAt, updatedAt]
      properties:
        id: {type: string, format: uuid}
        purpose: {type: string, enum: [ESCROW_FUND, MARK_PAYMENT, CANCEL_BEFORE_PAYMENT, EXPIRE, RELEASE, OPEN_DISPUTE, RESOLVE_DISPUTE]}
        transactionHash: {type: string, pattern: '^0x[a-fA-F0-9]{64}$'}
        status: {type: string, enum: [SUBMITTED, MINED, CONFIRMED, FAILED, REORGED]}
        blockNumber: {type: [string, 'null']}
        confirmations: {type: integer, minimum: 0}
        requiredConfirmations: {type: integer, minimum: 1}
        failureReason: {type: [string, 'null']}
        confirmedAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    PreparedWalletTransaction:
      type: object
      required: [kind, to, data, value]
      properties:
        kind: {type: string, enum: [APPROVAL_RESET, APPROVAL, ESCROW_FUND, MARK_PAYMENT, CANCEL_BEFORE_PAYMENT, EXPIRE, RELEASE, OPEN_DISPUTE, RESOLVE_DISPUTE]}
        to: {type: string, pattern: '^0x[a-fA-F0-9]{40}$'}
        data: {type: string, pattern: '^0x[a-fA-F0-9]+$'}
        value: {type: string, enum: ['0x0']}
    PreparedEscrowFundingAction:
      type: object
      required: [chainId, tradeKey, asset, token, tokenDecimals, grossAmount, grossAmountAtomic, fundingSource, authorizationExpiresAt, approvals, funding]
      properties:
        chainId: {type: integer, enum: [56, 97]}
        tradeKey: {type: string, pattern: '^0x[a-fA-F0-9]{64}$'}
        asset: {type: string, enum: [BNB, USDT]}
        token: {type: string, pattern: '^0x[a-fA-F0-9]{40}$'}
        tokenDecimals: {type: integer, minimum: 0, maximum: 36}
        grossAmount: {type: string}
        grossAmountAtomic: {type: string, pattern: '^\d+$'}
        fundingSource: {type: string, enum: [VAULT_BALANCE, WALLET]}
        authorizationExpiresAt: {type: string, format: date-time}
        approvals: {type: array, items: {$ref: '#/components/schemas/PreparedWalletTransaction'}}
        funding: {$ref: '#/components/schemas/PreparedWalletTransaction'}
    PreparedChainLifecycleAction:
      type: object
      required: [chainId, purpose, expectedEvent, expectedOutcome, transaction]
      properties:
        chainId: {type: integer, enum: [56, 97]}
        purpose: {type: string, enum: [MARK_PAYMENT, CANCEL_BEFORE_PAYMENT, EXPIRE, RELEASE, OPEN_DISPUTE]}
        expectedEvent: {type: string, enum: [PaymentMarked, TradeSettled, DisputeOpened]}
        expectedOutcome: {type: string, enum: [PAYMENT_MARKED, CANCELLED, EXPIRED, RELEASED, DISPUTED]}
        transaction: {$ref: '#/components/schemas/PreparedWalletTransaction'}
    PreparedDisputeSettlement:
      type: object
      required: [chainId, purpose, expectedEvent, expectedOutcome, decision, buyerAwardBps, rulingHash, transaction]
      properties:
        chainId: {type: integer, enum: [56, 97]}
        purpose: {type: string, enum: [RESOLVE_DISPUTE]}
        expectedEvent: {type: string, enum: [TradeSettled]}
        expectedOutcome: {type: string, enum: [RELEASED, REFUNDED]}
        decision: {type: string, enum: [RELEASE_TO_BUYER, REFUND_TO_SELLER]}
        buyerAwardBps: {type: integer, enum: [0, 10000]}
        rulingHash: {type: string, pattern: '^0x[a-fA-F0-9]{64}$'}
        transaction: {$ref: '#/components/schemas/PreparedWalletTransaction'}
    CreateTradeQuoteRequest:
      type: object
      additionalProperties: false
      required: [offerId, fiatAmount, paymentMethodId]
      properties:
        offerId: {type: string}
        fiatAmount: {type: string, pattern: '^\d+(\.\d+)?$'}
        paymentMethodId: {type: string, minLength: 1, maxLength: 100}
        sellerBankAccountId: {type: [string, 'null'], format: uuid, description: Required when the taker sells crypto into a bank-transfer buy offer.}
    TradeQuote:
      type: object
      required: [id, offerId, grossCryptoAmount, buyerFeeBps, buyerFeeAmount, buyerNetAmount, sellerFeeAmount, expiresAt, confirmationToken]
      properties:
        id: {type: string}
        offerId: {type: string}
        grossCryptoAmount: {type: string}
        buyerFeeBps: {type: integer, enum: [0, 100]}
        buyerFeeAmount: {type: string, description: One percent only when the offer-owning vendor is buying crypto.}
        buyerNetAmount: {type: string}
        sellerFeeAmount: {type: string, description: Always zero}
        expiresAt: {type: string, format: date-time}
        confirmationToken: {type: string}
    ReleaseIntent:
      type: object
      required: [tradeId, seller, buyer, token, amount, chainId, escrowContract, nonce, deadline, signature]
      properties:
        tradeId: {type: string}
        seller: {type: string}
        buyer: {type: string}
        token: {type: string}
        amount: {type: string}
        chainId: {type: integer, enum: [56, 97]}
        escrowContract: {type: string}
        nonce: {type: string}
        deadline: {type: integer}
        signature: {type: string}
    OpenDisputeRequest:
      type: object
      additionalProperties: false
      required: [reason, statement]
      properties:
        reason: {type: string, enum: [PAYMENT_NOT_RECEIVED, PAYMENT_SENT_NOT_RELEASED, WRONG_AMOUNT, PAYMENT_ACCOUNT_ISSUE, GIFT_CARD_INVALID, OTHER]}
        statement: {type: string, minLength: 20, maxLength: 4000}
    Dispute:
      type: object
      required: [id, reference, tradeId, openedByUserId, reason, statement, status, version, openedAt, updatedAt]
      properties:
        id: {type: string}
        reference: {type: string, pattern: '^\d+$'}
        tradeId: {type: string}
        openedByUserId: {type: string}
        reason: {type: string, enum: [PAYMENT_NOT_RECEIVED, PAYMENT_SENT_NOT_RELEASED, WRONG_AMOUNT, PAYMENT_ACCOUNT_ISSUE, GIFT_CARD_INVALID, OTHER]}
        statement: {type: string}
        status: {type: string, enum: [OPEN, ASSIGNED, AWAITING_MULTISIG, RESOLVED]}
        assignedModeratorId: {type: [string, 'null']}
        recommendation: {type: [string, 'null'], enum: [RELEASE_TO_BUYER, REFUND_TO_SELLER, null]}
        recommendationRationale: {type: [string, 'null']}
        recommendedByStaffId: {type: [string, 'null']}
        resolvedByStaffId: {type: [string, 'null']}
        resolutionTransactionHash: {type: [string, 'null']}
        version: {type: integer, minimum: 1}
        openedAt: {type: string, format: date-time}
        assignedAt: {type: [string, 'null'], format: date-time}
        recommendedAt: {type: [string, 'null'], format: date-time}
        resolvedAt: {type: [string, 'null'], format: date-time}
        updatedAt: {type: string, format: date-time}
    StaffLoginRequest:
      type: object
      additionalProperties: false
      required: [username, pin]
      properties:
        username: {type: string, minLength: 3, maxLength: 40}
        pin: {type: string, pattern: '^\d{6,12}$', writeOnly: true}
    CreateModeratorRequest:
      type: object
      additionalProperties: false
      required: [username, displayName, pin]
      properties:
        username: {type: string, pattern: '^[a-z0-9._-]{3,40}$'}
        displayName: {type: string, minLength: 2, maxLength: 80}
        pin: {type: string, pattern: '^\d{6,12}$', writeOnly: true}
    RotateModeratorPinRequest:
      type: object
      additionalProperties: false
      required: [pin]
      properties:
        pin: {type: string, pattern: '^\d{6,12}$', writeOnly: true}
    StaffIdentity:
      type: object
      required: [id, username, displayName, role, status, createdAt, updatedAt]
      properties:
        id: {type: string}
        username: {type: string}
        displayName: {type: string}
        role: {type: string, enum: [ADMIN, MODERATOR]}
        status: {type: string, enum: [ACTIVE, SUSPENDED]}
        lastLoginAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    StaffDisputeView:
      allOf:
        - {$ref: '#/components/schemas/Dispute'}
        - type: object
          properties:
            tradeReference: {type: [string, 'null']}
            openedByRole: {type: [string, 'null'], enum: [BUYER, SELLER, null]}
            trade:
              oneOf:
                - {$ref: '#/components/schemas/StaffTradeSnapshot'}
                - {type: 'null'}
            assignedModerator: {oneOf: [{$ref: '#/components/schemas/StaffIdentity'}, {type: 'null'}]}
            recommendedBy: {oneOf: [{$ref: '#/components/schemas/StaffIdentity'}, {type: 'null'}]}
            resolvedBy: {oneOf: [{$ref: '#/components/schemas/StaffIdentity'}, {type: 'null'}]}
            messages: {type: array, items: {$ref: '#/components/schemas/Message'}}
            evidence: {type: array, items: {$ref: '#/components/schemas/Evidence'}}
    AssignDisputeRequest:
      type: object
      additionalProperties: false
      required: [expectedVersion]
      properties:
        moderatorId: {type: string}
        expectedVersion: {type: integer, minimum: 1}
    RecommendDisputeRequest:
      type: object
      additionalProperties: false
      required: [expectedVersion, decision, rationale]
      properties:
        expectedVersion: {type: integer, minimum: 1}
        decision: {type: string, enum: [RELEASE_TO_BUYER, REFUND_TO_SELLER]}
        rationale: {type: string, minLength: 20, maxLength: 4000}
    StaffAuditEvent:
      type: object
      required: [id, action, entityType, entityId, metadata, createdAt]
      properties:
        id: {type: string}
        actorStaffId: {type: [string, 'null']}
        actor: {oneOf: [{$ref: '#/components/schemas/StaffIdentity'}, {type: 'null'}]}
        action: {type: string}
        entityType: {type: string, enum: [STAFF, DISPUTE, SESSION, PAYMENT_METHOD, GIFT_CARD_BRAND, GIFT_CARD_VENDOR]}
        entityId: {type: string}
        tradeId: {type: [string, 'null']}
        disputeId: {type: [string, 'null']}
        metadata: {type: object, additionalProperties: true}
        createdAt: {type: string, format: date-time}
    StaffTradeSnapshot:
      type: object
      required: [id, reference, asset, fiat, fiatAmount, grossCryptoAmount, paymentMethodId, state, buyerUserId, sellerUserId, createdAt]
      properties:
        id: {type: string}
        reference: {type: string}
        asset: {type: string, enum: [BNB, USDT]}
        fiat: {type: string}
        fiatAmount: {type: string}
        grossCryptoAmount: {type: string}
        paymentMethodId: {type: string}
        state: {type: string}
        buyerUserId: {type: string}
        sellerUserId: {type: string}
        paymentMarkedAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
    PaymentMethod:
      type: object
      required: [id, code, name, category, region, markets, currencies, speed, risk, aliases, featured, status, version, createdByStaffId, createdAt, updatedAt]
      properties:
        id: {type: string}
        code: {type: string, pattern: '^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$'}
        name: {type: string}
        category: {type: string, enum: [BANK_TRANSFER, INSTANT_BANK, MOBILE_MONEY, DIGITAL_WALLET, CASH_VOUCHER, CARD_PREPAID]}
        region: {type: string, enum: [GLOBAL, AFRICA, NORTH_AMERICA, LATIN_AMERICA, EUROPE, ASIA, MIDDLE_EAST, OCEANIA]}
        markets: {type: array, items: {type: string}}
        currencies: {type: array, items: {type: string}}
        speed: {type: string, enum: [INSTANT, MINUTES, SAME_DAY, ONE_TO_THREE_DAYS]}
        risk: {type: string, enum: [STANDARD, ELEVATED, RESTRICTED]}
        aliases: {type: array, items: {type: string}}
        featured: {type: boolean}
        status: {type: string, enum: [ACTIVE, DISABLED]}
        version: {type: integer, minimum: 1}
        createdByStaffId: {type: string}
        createdAt: {type: string, format: date-time}
        updatedAt: {type: string, format: date-time}
    CreatePaymentMethodRequest:
      allOf:
        - {$ref: '#/components/schemas/PaymentMethodFields'}
        - type: object
          required: [code, name, category, region, markets, currencies, speed, risk]
          properties:
            code: {type: string, pattern: '^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$'}
    UpdatePaymentMethodRequest:
      allOf:
        - type: object
          required: [expectedVersion]
          properties:
            expectedVersion: {type: integer, minimum: 1}
        - {$ref: '#/components/schemas/PaymentMethodFields'}
    PaymentMethodFields:
      type: object
      properties:
        name: {type: string, minLength: 2, maxLength: 80}
        category: {type: string, enum: [BANK_TRANSFER, INSTANT_BANK, MOBILE_MONEY, DIGITAL_WALLET, CASH_VOUCHER, CARD_PREPAID]}
        region: {type: string, enum: [GLOBAL, AFRICA, NORTH_AMERICA, LATIN_AMERICA, EUROPE, ASIA, MIDDLE_EAST, OCEANIA]}
        markets: {type: array, minItems: 1, maxItems: 50, items: {type: string}}
        currencies: {type: array, minItems: 1, maxItems: 50, items: {type: string}}
        speed: {type: string, enum: [INSTANT, MINUTES, SAME_DAY, ONE_TO_THREE_DAYS]}
        risk: {type: string, enum: [STANDARD, ELEVATED, RESTRICTED]}
        aliases: {type: array, maxItems: 30, items: {type: string}}
        featured: {type: boolean}
        status: {type: string, enum: [ACTIVE, DISABLED]}
    Message:
      type: object
      required: [id, tradeId, sequence, kind, body, sender, senderRole, evidence, createdAt]
      properties:
        id: {type: string, format: uuid}
        tradeId: {type: string}
        sequence: {type: integer}
        senderUserId: {type: [string, 'null'], format: uuid}
        kind: {type: string, enum: [USER, SYSTEM]}
        sender: {type: string, enum: [ME, COUNTERPARTY, SYSTEM]}
        senderRole: {type: string, enum: [BUYER, SELLER, SYSTEM]}
        body: {type: string}
        clientMessageId: {type: [string, 'null'], format: uuid}
        evidence: {type: array, items: {$ref: '#/components/schemas/Evidence'}}
        createdAt: {type: string, format: date-time}
        preservationLockedAt: {type: [string, 'null'], format: date-time}
    CreateEvidenceRequest:
      type: object
      additionalProperties: false
      required: [purpose, fileName, contentType, byteSize, sha256]
      properties:
        purpose: {type: string, enum: [CHAT, PAYMENT_PROOF, GIFT_CARD, DISPUTE]}
        fileName: {type: string, minLength: 1, maxLength: 255}
        contentType: {type: string, minLength: 3, maxLength: 120}
        byteSize: {type: integer, minimum: 1, maximum: 20971520}
        sha256: {type: string, pattern: '^[a-fA-F0-9]{64}$'}
    Evidence:
      type: object
      required: [id, tradeId, uploaderUserId, purpose, fileName, contentType, byteSize, sha256, status, createdAt]
      properties:
        id: {type: string, format: uuid}
        tradeId: {type: string, format: uuid}
        uploaderUserId: {type: string, format: uuid}
        messageId: {type: [string, 'null'], format: uuid}
        purpose: {type: string, enum: [CHAT, PAYMENT_PROOF, GIFT_CARD, DISPUTE]}
        fileName: {type: string}
        contentType: {type: string}
        byteSize: {type: integer}
        sha256: {type: string}
        status: {type: string, enum: [AWAITING_UPLOAD, QUARANTINED, AVAILABLE, REJECTED, DELETED]}
        preservationLockedAt: {type: [string, 'null'], format: date-time}
        createdAt: {type: string, format: date-time}
        uploadedAt: {type: [string, 'null'], format: date-time}
        scannedAt: {type: [string, 'null'], format: date-time}
    EvidenceAccessAudit:
      type: object
      required: [id, evidenceId, tradeId, action, requestId, ipHash, createdAt]
      properties:
        id: {type: string, format: uuid}
        evidenceId: {type: string, format: uuid}
        tradeId: {type: string, format: uuid}
        actorUserId: {type: [string, 'null'], format: uuid}
        actorStaffId: {type: [string, 'null'], format: uuid}
        action: {type: string, enum: [PARTICIPANT_DOWNLOAD, STAFF_DOWNLOAD]}
        requestId: {type: string}
        ipHash: {type: string, pattern: '^[a-f0-9]{64}$'}
        createdAt: {type: string, format: date-time}
    ProtocolStats:
      type: object
      properties:
        volume24h: {type: string}
        volume30d: {type: string}
        completedTrades: {type: integer}
        activeOffers: {type: integer}
        disputeRate: {type: number}
        observedBlock: {type: integer}
    CursorPage:
      type: object
      properties:
        nextCursor: {type: [string, 'null']}
        hasMore: {type: boolean}
    Problem:
      type: object
      required: [type, title, status, code]
      properties:
        type: {type: string, format: uri}
        title: {type: string}
        status: {type: integer}
        code: {type: string}
        detail: {type: string}
        traceId: {type: string}
