openapi: 3.1.0
info:
  title: ChatterPay B2B API
  version: 0.13.0
  description: Contrato público de la suite 0.13.0. Incluye transferencias directas, LI.FI, Base, Arbitrum y Bitcoin; perfiles
    mock, Anvil, testnet y configuración de producción; rotación de validator, recuperación de root y notificaciones desacopladas.
    Las capacidades no verificadas fuera del entorno local se identifican expresamente.
x-spec-status: accepted-for-pilot
servers:
- url: http://127.0.0.1:4000
  description: Local developer API
- url: https://REPLACE_WITH_GCP_API_URL
  description: Optional GCP development API
paths:
  /health:
    get:
      security: []
      operationId: getHealth
      summary: Get API and worker health
      responses:
        '200':
          description: Health state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
  /oauth/token:
    post:
      security: []
      operationId: issueAccessToken
      summary: Issue an access token with the OAuth 2.0 Client Credentials grant
      description: >-
        One credential per consuming application (`DEC-013`, `GAP-007`): a Partner backend, a bot,
        the sandbox BFF and the dashboard are separate clients, each with its own `clientId`,
        `clientSecret`, tenant, environment and scopes.


        The token is short-lived and its advertised `expiresIn` is the lifetime actually enforced.
        There is **no refresh token and no refresh grant**: a consumer that needs a new token
        presents its client credentials again. A non-expiring token cannot be requested or
        configured, and `expiresIn` is never negative or null.


        The credential is verified against the deployment's shared client registry, so a client
        rotated or revoked through the administrative surface — on any replica — is refused here
        immediately.


        Two ways to present a credential, and the same token comes out of both. `clientSecret` is
        the sandbox grant `DEC-013` allows; `clientAssertion` is `private_key_jwt`, the productive
        one. In a production deployment only the sandbox BFF may present a secret. Every refusal —
        unknown client, unknown or retired key, wrong issuer, subject or audience, expired, an
        over-long declared lifetime, a missing `jti`, or a replay of a valid assertion — answers the
        same `401`, so a probe cannot learn which check it passed.
      x-pilot-only: true
      x-token-ttl-seconds: 3600
      x-clock-skew-seconds: 60
      x-refresh-token: none
      x-max-assertion-lifetime-seconds: 300
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - clientId
              properties:
                clientId:
                  type: string
                clientSecret:
                  description: >-
                    The sandbox grant. Limited to sandbox by `DEC-013`; in a production deployment
                    only the sandbox BFF may present one.
                  type: string
                clientAssertion:
                  description: >-
                    An RFC 7523 client assertion, signed with a key the client registered. Present
                    it instead of `clientSecret`; a body carrying both is answered by taking this
                    one, so a downgrade cannot be offered as a choice.
                  type: string
                clientAssertionType:
                  description: Optional, and when present it must be the RFC 7523 value.
                  type: string
                  enum:
                  - urn:ietf:params:oauth:client-assertion-type:jwt-bearer
      responses:
        '200':
          description: Access token
          content:
            application/json:
              schema:
                type: object
                required:
                - accessToken
                - tokenType
                - expiresIn
                properties:
                  accessToken:
                    type: string
                  tokenType:
                    type: string
                    const: Bearer
                  expiresIn:
                    description: >-
                      Lifetime in seconds, always a positive integer. The deployment default is
                      3600 and a verifier tolerates at most 60 seconds of clock skew past it.
                    type: integer
                    minimum: 1
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/admin/clients:
    post:
      operationId: createApiClient
      x-required-scope: clients:manage
      summary: Register a consuming application
      description: >-
        Creates a client and returns its `clientSecret` **once**, in this response and nowhere
        else. The API stores only an irreversible digest, so the plaintext cannot be recovered
        afterwards by any endpoint, backup or support path; a caller that loses it rotates.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [clientId]
              properties:
                clientId:
                  type: string
                  pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$'
                label:
                  type: string
                  description: Human-readable name for logs and diagnostics. Never a secret.
                tenantId:
                  type: string
                application:
                  type: string
                  description: Which consuming application this is (`partner-backend`, `bot`, …).
                scopes:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum: [operations, internal, clients:manage]
      responses:
        '201':
          description: The client, and its secret for the only time it is visible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedApiClient'
        '400':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
        '409':
          $ref: '#/components/responses/ProblemResponse'
    get:
      operationId: listApiClients
      x-required-scope: clients:manage
      summary: List registered clients
      description: >-
        Never returns a secret, and never returns a secret digest either. The digest is not the
        secret, but it is enough to verify a guess offline, so it is not part of this contract.
      responses:
        '200':
          description: The registered clients
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiClient'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/admin/clients/{clientId}/rotate:
    post:
      operationId: rotateApiClientSecret
      x-required-scope: clients:manage
      summary: Rotate a client's secret
      description: >-
        Generates a new secret, increments `credentialVersion` and persists both before answering.
        The previous secret stops authenticating and every access token issued under it stops
        authorising — on every replica, without waiting out the token lifetime.
      parameters:
      - $ref: '#/components/parameters/ClientId'
      responses:
        '200':
          description: The client, and its new secret for the only time it is visible
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedApiClient'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
        '404':
          $ref: '#/components/responses/ProblemResponse'
  /v1/admin/clients/{clientId}/revoke:
    post:
      operationId: revokeApiClient
      x-required-scope: clients:manage
      summary: Revoke a client
      description: >-
        Sets the status to `revoked`, increments `credentialVersion` and persists before
        answering. Live access tokens stop authorising and no new token can be obtained. A revoked
        client is not resurrected by a restart, even when the deployment's bootstrap configuration
        still names it.
      parameters:
      - $ref: '#/components/parameters/ClientId'
      responses:
        '200':
          description: The revoked client
          content:
            application/json:
              schema:
                type: object
                required: [client]
                properties:
                  client:
                    $ref: '#/components/schemas/ApiClient'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
        '404':
          $ref: '#/components/responses/ProblemResponse'
  /v1/assets:
    get:
      operationId: listAssets
      x-required-scope: operations
      summary: List pilot asset representations
      responses:
        '200':
          description: Asset representations
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/AssetRepresentation'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/users:
    post:
      operationId: createUser
      x-required-scope: operations
      summary: Create a tenant-scoped user
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - externalUserId
              properties:
                externalUserId:
                  type: string
                  minLength: 1
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantUser'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/recipient-aliases:
    post:
      operationId: registerRecipientAlias
      x-required-scope: operations
      summary: Register a tenant-scoped recipient alias
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [userId, type, value]
              properties:
                userId:
                  type: string
                type:
                  type: string
                  enum: [phone]
                value:
                  type: string
                  description: Phone number in E.164 form.
      responses:
        '201':
          description: Alias registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecipientAlias'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/recipients/resolve:
    post:
      operationId: resolveRecipient
      x-required-scope: operations
      summary: Resolve a human recipient alias to the requested chain accounts
      description: >-
        A pure read: it never writes on chain and never persists a chain account. Where the address
        is knowable without provisioning it is reported anyway, marked as not yet provisioned, which
        is what lets a caller show one EVM address across every EVM network before the first
        egress activates it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [type, value]
              properties:
                type:
                  type: string
                  enum: [phone]
                value:
                  type: string
                chainId:
                  description: >-
                    Single network to resolve. Kept for compatibility; prefer chainIds. The top
                    level chainId, status, address and chainAccountId of the response describe this
                    network, or the first requested one.
                  type: string
                  enum:
                  - arbitrum-sepolia
                  - base-sepolia
                  - bitcoin-regtest
                  - bitcoin-testnet4
                  - arbitrum-one
                  - base-mainnet
                  - bitcoin-mainnet
                  - cardano-preprod
                  - cardano-mainnet
                chainIds:
                  description: >-
                    Networks to resolve in one call. Omitted, it resolves every active network of
                    the deployment.
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum:
                    - arbitrum-sepolia
                    - base-sepolia
                    - bitcoin-regtest
                    - bitcoin-testnet4
                    - arbitrum-one
                    - base-mainnet
                    - bitcoin-mainnet
                    - cardano-preprod
                    - cardano-mainnet
      responses:
        '200':
          description: Resolution result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecipientResolution'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/recipients:
    post:
      operationId: provisionRecipient
      x-required-scope: operations
      summary: Resolve a recipient alias, creating the recipient when it does not exist
      description: >-
        Explicit counterpart to the pure read at /v1/recipients/resolve. Creates the tenant user,
        wallet profile, chain account and alias that are missing, and returns the recipient ready
        to receive on the requested chain. Every step is find-or-create, so a retry converges on
        the same recipient instead of producing a second wallet for the same person.
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [type, value, chainId]
              properties:
                type:
                  type: string
                  enum: [phone]
                value:
                  type: string
                chainId:
                  type: string
                  enum:
                  - arbitrum-sepolia
                  - base-sepolia
                  - bitcoin-regtest
                  - bitcoin-testnet4
                  - arbitrum-one
                  - base-mainnet
                  - bitcoin-mainnet
                  - cardano-preprod
                  - cardano-mainnet
      responses:
        '200':
          description: The recipient already existed on the requested chain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedRecipient'
        '201':
          description: The recipient, or the missing part of it, was created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionedRecipient'
        '409':
          description: The recipient alias is blocked and may not receive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets:
    post:
      operationId: createWallet
      x-required-scope: operations
      summary: Create a logical wallet profile
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - userId
              properties:
                userId:
                  type: string
      responses:
        '201':
          description: Wallet created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletProfile'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets/{walletId}/accounts:
    get:
      operationId: listWalletAccounts
      x-required-scope: operations
      summary: Read every chain account of a wallet in one call
      description: >-
        A pure read, and the counterpart of the multi-network recipient resolution for a caller
        that already holds the walletId. One row per active network of the deployment: provisioned
        networks answer with their persisted account, and a network the wallet has not been
        provisioned on reports the address it would hold, marked as not provisioned, without
        writing anything. Answering "which are my addresses" therefore costs one call rather than
        one per network, and the EVM rows show the single shared address instead of asserting it.
      parameters:
      - $ref: '#/components/parameters/WalletId'
      responses:
        '200':
          description: The wallet's accounts, one row per active network
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletAccounts'
        '404':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
    post:
      operationId: provisionAccount
      x-required-scope: operations
      summary: Provision a simulated or EIP-7702 harness account
      parameters:
      - $ref: '#/components/parameters/WalletId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                chainId:
                  type: string
                  enum: &id001
                  - arbitrum-sepolia
                  - base-sepolia
                  - bitcoin-regtest
                  - bitcoin-testnet4
                  - arbitrum-one
                  - base-mainnet
                  - bitcoin-mainnet
                  - cardano-preprod
                  - cardano-mainnet
                  default: arbitrum-sepolia
      responses:
        '200':
          description: Existing account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainAccount'
        '201':
          description: Account provisioned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainAccount'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/deposit-routes:
    post:
      operationId: createDepositRoute
      x-required-scope: operations
      summary: Resolve a deposit address for a wallet and asset
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - walletId
              - assetId
              properties:
                walletId:
                  type: string
                assetId:
                  type: string
      responses:
        '201':
          description: Deposit route
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DepositRoute'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/sandbox/faucet:
    post:
      operationId: createFaucetOperation
      x-required-scope: operations
      summary: Create a development-only deposit operation
      description: >-
        Mints test balances. It does not exist in a production deployment: there the route answers
        `404`, indistinguishable from a path that was never implemented, because a deployment
        holding real value must not offer a way to create balances out of nothing.
      x-pilot-only: true
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - walletId
              - assetId
              properties:
                walletId:
                  type: string
                assetId:
                  type: string
                amount:
                  allOf:
                  - $ref: '#/components/schemas/DecimalAmount'
                  description: >-
                    Optional. When omitted, the environment's own policy amount for that asset is
                    used. A caller that should not be choosing an amount -- the conversational
                    channel, where the number would be produced by a model -- omits it. An asset
                    this solution does not issue has no policy amount and is refused with
                    `ASSET_NOT_FUNDABLE`.
                failureMode:
                  $ref: '#/components/schemas/FailureMode'
      responses:
        '202':
          description: Deposit accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operation'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
        '409':
          description: >-
            This deployment cannot mint test balances on that network, and says so here rather than
            accepting an operation it already knows will fail (`CAP-070` REQ-070-07). `code` is
            `TEST_FUNDING_NOT_CONFIGURED` and `retryable` is `false`: repeating the request changes
            nothing until the environment is configured. No operation is created and no grant is
            consumed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemResponse'
  /v1/balances:
    get:
      operationId: listBalances
      x-required-scope: operations
      summary: List ledger-projected pilot balances
      parameters:
      - name: walletId
        in: query
        required: true
        schema:
          type: string
      responses:
        '404':
          description: The wallet does not exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemResponse'
        '200':
          description: Balance page
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Balance'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/quotes:
    post:
      operationId: createQuote
      x-required-scope: operations
      summary: Create a five-minute transfer quote
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - walletId
              - assetId
              - amount
              - destination
              properties:
                walletId:
                  type: string
                assetId:
                  type: string
                amount:
                  $ref: '#/components/schemas/DecimalAmount'
                destination:
                  type: string
                toChainId:
                  type: string
                  enum: *id001
                toAssetId:
                  type: string
                  description: >-
                    What to receive, when it differs from what is sent. Either a representation
                    identifier, which names its network, or a bare symbol, which does not: a symbol
                    resolves to the one representation of it on the destination network -- the one
                    `toChainId` names, or the one the send leaves from. A symbol therefore asks for
                    a same-chain swap and leaves the source free to be re-pointed, exactly as a
                    plain send is; a representation of another network asks for a bridge. A symbol
                    the destination network does not carry is `UNKNOWN_ASSET`, and one is never
                    resolved across networks.
                settlementProvider:
                  type: string
                  enum:
                  - direct
                  - lifi
                  - deployment_pool
                operationType:
                  $ref: '#/components/schemas/OperationType'
                  description: Optional explicit operation type. The current transfer quote path executes direct_transfer,
                    swap and cross_chain_transfer; other published policies are capability-gated.
                notificationTarget:
                  $ref: '#/components/schemas/NotificationTarget'
                slippage:
                  type: number
                  exclusiveMinimum: 0
                  description: >-
                    The share of the output the caller accepts losing between pricing and
                    settlement, as a decimal fraction: `0.005` is half a percent. It applies only
                    where a third-party router prices the route, and it is the difference between a
                    route that fails and one that fills at a worse price -- which of those is
                    preferable belongs to the caller. Omitted, the quote is priced at the
                    deployment's default; above the deployment's maximum, the request is refused
                    with `SLIPPAGE_OUT_OF_RANGE` stating that maximum. Either way the quote states
                    the value it was priced under (`CAP-050` REQ-050-11).
      responses:
        '201':
          description: Quote created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
        '409':
          description: >-
            The wallet cannot fund the requested amount (`INSUFFICIENT_BALANCE`). Refused here
            rather than at execution: a quote is what a consumer shows a user and asks them to
            confirm, so it must not price an operation that is already known to be unexecutable.
            `details` carries `required`/`available` in both decimal and base units. A quote is
            not a reservation, so an affordable quote can still be refused at transfer time.


            Also `FUNDS_NOT_YET_SPENDABLE` (`CAP-050` AC-050-09) when the ledger holds the amount
            and the chain will not let the account spend it yet. On a UTXO family the two disagree
            for as long as the confirmation threshold lasts: a deposit is credited when its
            operation completes, and input selection only takes outputs already deep enough to be
            firm. `details` carries `required`, `spendableOnChain`, `ledgerAvailable` and the
            `address` read, in both decimal and base units, because the difference between the two
            readings is what tells a user to wait rather than to add funds. `retryable` is `true`:
            the same request succeeds once the deposit is deep enough.


            Also `AMOUNT_BELOW_MINIMUM_TRANSFERABLE`, `INSUFFICIENT_FUNDS_FOR_NETWORK_FEE` and
            `AMOUNT_WOULD_BURN_CHANGE` (`CAP-050` AC-050-11) on a chain that states what it would
            do with this transfer rather than only what the account holds. A UTXO family puts a
            minimum under the output carrying the amount, takes the network fee out of the same
            inputs, and has a band between the two where the transfer settles and burns its own
            leftover into the fee because that leftover is too small to be returned as change. The
            first two would surface at the builder, after the user confirmed; the third would not
            surface at all. `details` carries the operands in decimal and base units, and for the
            burn it carries `maximumKeepingChange` and `sendEverything` -- the two amounts that
            avoid it -- because the band itself is not something a person can act on.


            Also `WALLET_ADDRESS_NOT_SIGNABLE` (`CAP-070` AC-070-11) when no key this deployment
            holds authorises the account the funds are in: `details` carries `address` and
            `derivedAddress` so the disagreement is readable without server logs, and `retryable`
            is `false` — no retry, no wait and no amount of gas makes a key sign for an address it
            does not control, and the account has to be re-provisioned. And `ACCOUNT_MODEL_RETIRED`
            when the account belongs to an account model this deployment withdrew, which is the
            same answer for the same reason.


            Also `ACCOUNT_DEPLOYMENT_MISMATCH` (`CAP-020` AC-020-09) when the account's signing
            identity belongs to a different deployment than the one this process serves, or was
            never recorded. `details` carries `accountDeploymentId` and `servedDeploymentId`. A
            deployment does not adapt another deployment's account to itself: doing so would make
            the account signable for a different address and say nothing about it. The signing
            identity itself is never returned — it names a key, and naming a key is not something a
            public surface needs to do.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/transfers:
    post:
      operationId: createTransfer
      x-required-scope: operations
      summary: Accept an asynchronous transfer operation
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - quoteId
              properties:
                quoteId:
                  type: string
                failureMode:
                  $ref: '#/components/schemas/FailureMode'
      responses:
        '202':
          description: Transfer accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operation'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/operations:
    get:
      operationId: listOperations
      x-required-scope: operations
      summary: List pilot operations
      description: >-
        Newest first. `walletId` narrows the list to one wallet's operations and never widens it:
        the same credential reads every operation of the deployment from this endpoint unfiltered,
        so a caller passing it sees strictly less.

        It exists because a conversational surface cannot carry an identifier across turns. A tool
        result does not survive into the next message, so the only place an operation identifier
        exists for such a channel is the message it wrote itself — and when it wrote none, the
        next question about that transfer is unanswerable about an operation the platform completed
        and is holding.
      parameters:
      - name: walletId
        in: query
        required: false
        schema:
          type: string
        description: Only this wallet's operations.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 9999
        description: >-
          At most this many, counted from the newest. A value that is not a whole count of rows is
          refused rather than ignored: a caller asking for the last one and silently receiving the
          whole deployment is what this parameter exists to avoid.
      responses:
        '200':
          description: Operation page
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Operation'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/operations/{operationId}:
    get:
      operationId: getOperation
      x-required-scope: operations
      summary: Get operation state
      parameters:
      - $ref: '#/components/parameters/OperationId'
      responses:
        '200':
          description: Operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Operation'
        '404':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/operations/{operationId}/steps:
    get:
      operationId: listOperationSteps
      x-required-scope: operations
      summary: Get the worker timeline for an operation
      parameters:
      - $ref: '#/components/parameters/OperationId'
      responses:
        '200':
          description: Operation steps
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/OperationStep'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/operations/{operationId}/legs:
    get:
      operationId: listOperationLegs
      x-required-scope: operations
      summary: Get the per-chain settlement legs of a LI.FI operation
      description: |-
        Empty for a direct (non-settlement) operation. A LI.FI settlement records one leg per
        chain it actually touches (source swap/bridge, destination swap/final transfer), each
        with its own provider route id, amount, transaction hash, and status, so a partial
        completion, refund, or discrepancy can be observed and reconciled per leg instead of only
        at the whole-operation level.
      parameters:
      - $ref: '#/components/parameters/OperationId'
      responses:
        '200':
          description: Operation legs
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/OperationLeg'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/events:
    get:
      operationId: listEvents
      x-required-scope: operations
      summary: Replay versioned events by cursor
      parameters:
      - name: cursor
        in: query
        schema:
          type: integer
          minimum: 0
          default: 0
      responses:
        '200':
          description: Event page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventPage'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/system/platform-accounts:
    get:
      operationId: listPlatformAccounts
      x-required-scope: operations
      summary: Read the balances of the accounts the platform funds and collects into, per chain
      description: >-
        The accounts that make "the user does not pay gas" true: the relayer, the operational
        validator, the Paymaster with its EntryPoint deposit and stake, and the EntryPoint itself.
        Alongside them, the revenue side of the same deployment: each Partner's `fee_collection`
        account with what has accrued to it and is not swept yet (`DEC-041`), one entry per Partner
        per chain. Every figure is read from the chain at call time — a stale funding number is
        worse than none, because it gets acted on. A chain whose adapter has no such accounts is
        reported with `available: false` and a reason rather than omitted.


        A deployment that funds the public sandbox separately reports **both** lanes, each row
        naming its own in `lane`: the account an operator has to watch drain is the one that is
        draining, and on such a deployment that is a different address from the pilot's.
      responses:
        '200':
          description: One entry per active chain
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/PlatformAccountsSnapshot'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/system/contracts:
    get:
      operationId: listChainContracts
      x-required-scope: operations
      summary: Read the contract deployment each chain is running under
      description: >-
        The contracts ChatterPay executes against on every EVM chain of this deployment: the account
        implementation, the transfer module, the EntryPoint, the Paymaster, the account factory and
        the asset representations. Derived from the deployment the running API loaded, so what is
        reported and what is executed against are the same addresses — a manifest read independently
        can describe a deployment this environment is not using. A chain with no EVM deployment here
        is reported with `available: false` and a reason rather than omitted.
      responses:
        '200':
          description: One entry per active chain
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ChainContractDeployment'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/system/overview:
    get:
      operationId: getPilotOverview
      x-required-scope: operations
      summary: Inspect operation, event, worker and outbox totals
      x-internal-pilot: true
      responses:
        '200':
          description: Pilot overview
          content:
            application/json:
              schema:
                type: object
                required:
                - version
                - profile
                properties:
                  version:
                    type: string
                    description: Version of the running API build, as declared by its own package manifest.
                  profile:
                    type: string
                    enum:
                    - mock
                    - anvil
                    - testnet
                    - production
                additionalProperties: true
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/internal/workers:
    get:
      operationId: listWorkers
      x-required-scope: internal
      summary: Inspect worker heartbeats and counters
      x-internal-pilot: true
      responses:
        '200':
          description: Worker states
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/WorkerState'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/internal/outbox:
    get:
      operationId: listOutbox
      x-required-scope: internal
      summary: Inspect pilot outbox items
      x-internal-pilot: true
      responses:
        '200':
          description: Outbox items
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/admin/clients/{clientId}/keys:
    post:
      operationId: addClientPublicKey
      x-required-scope: clients:manage
      summary: Register a public key a client may authenticate with
      description: >-
        The key a `private_key_jwt` assertion is checked against. Only the public half is ever sent:
        a JWK carrying private components is refused by name rather than stripped, because a Partner
        who sent one has exported the wrong half of their pair and must know it. Registering a key
        does not invalidate live tokens — adding a way to authenticate is not a reason to stop
        honouring authentications that already happened.
      x-internal-pilot: true
      parameters:
        - name: clientId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kid, alg, jwk]
              properties:
                kid:
                  description: Key id, matched against the assertion header's `kid`.
                  type: string
                alg:
                  type: string
                  enum: [ES256, RS256]
                jwk:
                  description: The public key in JWK form.
                  type: object
                  additionalProperties: true
      responses:
        '201':
          description: The client, with the key registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  client:
                    $ref: '#/components/schemas/ApiClient'
        '400':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '403':
          $ref: '#/components/responses/ProblemResponse'
        '404':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/admin/clients/{clientId}/keys/{kid}:
    delete:
      operationId: retireClientPublicKey
      x-required-scope: clients:manage
      summary: Retire a public key, and every token obtained with it
      description: >-
        Unlike registering, this bumps the credential version: a key is retired because it should no
        longer be trusted, and leaving the tokens it already obtained alive would let the compromise
        outlive the response to it.
      x-internal-pilot: true
      parameters:
        - name: clientId
          in: path
          required: true
          schema:
            type: string
        - name: kid
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The client, with the key retired
          content:
            application/json:
              schema:
                type: object
                properties:
                  client:
                    $ref: '#/components/schemas/ApiClient'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '403':
          $ref: '#/components/responses/ProblemResponse'
        '404':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/internal/reconciliation-report:
    get:
      operationId: getReconciliationReport
      x-required-scope: internal
      summary: Reconciliation outcomes for this tenant, grouped by chain, asset and result
      description: >-
        Counts the reconciliation records of the authenticated tenant over a period, grouped by
        chain, asset and outcome (`CAP-040` REQ-040-05). The tenant comes from the credential and
        is never a request parameter (`AC-040-06`): a caller cannot ask for another tenant's
        figures. Records written before a tenant was recorded on them are counted separately in
        `unattributed` rather than dropped, so an empty report is never read as "nothing to
        reconcile".
      x-internal-pilot: true
      parameters:
        - name: chainId
          in: query
          required: false
          schema:
            type: string
        - name: assetId
          in: query
          required: false
          schema:
            type: string
        - name: since
          in: query
          required: false
          description: Inclusive lower bound on when the check ran, RFC 3339.
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          required: false
          description: Exclusive upper bound, so consecutive periods neither overlap nor drop a record.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Grouped reconciliation outcomes
          content:
            application/json:
              schema:
                type: object
                required: [items, unattributed]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/ReconciliationReportGroup'
                  unattributed:
                    type: integer
                    description: >-
                      Records in the same window carrying no tenant. They belong to this deployment
                      by construction while V1 is single-tenant per deployment, and they are
                      reported apart rather than merged in.
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/internal/unattributed-deposits:
    get:
      operationId: listUnattributedDeposits
      x-required-scope: internal
      summary: Inbound transfers that arrived and could not be credited
      description: >-
        Transfers seen at an address this deployment watches that could not be attributed to a
        wallet and to an asset representation it declares (`CAP-030` REQ-030-09, AC-030-02). They
        credit nothing and they are kept: money that arrived and was neither credited nor recorded
        is money nobody can account for, so the alternative to this list is losing the fact that it
        arrived.


        An empty list means nothing arrived unattributed. It never means nothing was looked at:
        `observersReadTo` states how far each observer has processed, so a reader can tell an empty
        answer from an observer that has not run.
      x-internal-pilot: true
      parameters:
        - name: chainId
          in: query
          required: false
          schema:
            type: string
            enum: *id001
        - name: since
          in: query
          required: false
          description: Inclusive lower bound on when the transfer was observed, RFC 3339.
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          required: false
          description: Exclusive upper bound, so consecutive periods neither overlap nor drop a row.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Unattributed inbound transfers
          content:
            application/json:
              schema:
                type: object
                required: [items, observersReadTo]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/UnattributedDeposit'
                  observersReadTo:
                    type: array
                    description: >-
                      How far each chain observer has durably processed (`CAP-030` REQ-030-06). It
                      is what makes an empty list readable: an observer whose cursor has not moved
                      has not looked, and that is a different answer from nothing having arrived.
                    items:
                      type: object
                      required: [chainId]
                      properties:
                        chainId:
                          type: string
                          enum: *id001
                        blockHeight:
                          type: integer
                          description: The height the cursor stands at, where the family counts in blocks.
                        cursor:
                          type: string
                          description: >-
                            The cursor as that family expresses it, where a height does not describe
                            it — a slot and block hash on Cardano, for instance.
                        updatedAt:
                          type: string
                          format: date-time
                        simulated:
                          type: boolean
                          description: True when this observer is a simulated one.
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/internal/webhook-deliveries:
    get:
      operationId: listWebhookDeliveries
      x-required-scope: internal
      summary: Inspect signed-webhook delivery history
      x-internal-pilot: true
      responses:
        '200':
          description: Delivery history
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/chains:
    get:
      summary: List enabled chains, their account model and their execution protocols
      operationId: listChains
      x-required-scope: operations
      responses:
        '200':
          description: Enabled chains
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Chain'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/lifi/quotes:
    post:
      summary: Request a server-side LI.FI quote
      description: Uses an explicit simulator or server-side live read-only LI.FI API access. API keys never reach the browser.
      operationId: createLifiQuote
      x-required-scope: operations
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LifiQuoteRequest'
      responses:
        '200':
          description: Validated quote
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LifiQuote'
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/lifi/status:
    get:
      summary: Read LI.FI route status
      operationId: getLifiStatus
      x-required-scope: operations
      parameters:
      - name: txHash
        in: query
        required: true
        schema:
          type: string
      - name: fromChainId
        in: query
        required: true
        schema:
          type: string
      - name: toChainId
        in: query
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Provider status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LifiStatus'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/fee-policies:
    get:
      operationId: listFeePolicies
      x-required-scope: operations
      summary: List the active product fee matrix
      description: Returns service-fee basis points, provider-fee pass-through sources and network sponsorship mode for each
        operation type. Polymarket, AAVE and NFT policies are published before their execution capabilities are enabled.
      responses:
        '200':
          description: Fee policy catalog
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/FeePolicy'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets/{walletId}/accounts/{chainId}/rotate-validator:
    post:
      operationId: rotateOperationalValidator
      x-required-scope: operations
      summary: Rotate the operational validator without changing the account address
      parameters:
      - $ref: '#/components/parameters/WalletId'
      - $ref: '#/components/parameters/ChainId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - newValidator
              properties:
                newValidator:
                  type: string
                  pattern: ^0x[0-9a-fA-F]{40}$
      responses:
        '202':
          description: Acción aceptada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyActionResult'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets/{walletId}/accounts/{chainId}/recovery/schedule:
    post:
      operationId: scheduleRootRecovery
      x-required-scope: operations
      summary: Schedule delayed root-authority recovery
      parameters:
      - $ref: '#/components/parameters/WalletId'
      - $ref: '#/components/parameters/ChainId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - newRootAuthority
              properties:
                newRootAuthority:
                  type: string
                  pattern: ^0x[0-9a-fA-F]{40}$
      responses:
        '202':
          description: Acción aceptada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyActionResult'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets/{walletId}/accounts/{chainId}/recovery/cancel:
    post:
      operationId: cancelRootRecovery
      x-required-scope: operations
      summary: Cancel a pending root recovery
      parameters:
      - $ref: '#/components/parameters/WalletId'
      - $ref: '#/components/parameters/ChainId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties: {}
      responses:
        '202':
          description: Acción aceptada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyActionResult'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets/{walletId}/accounts/{chainId}/recovery/execute:
    post:
      operationId: executeRootRecovery
      x-required-scope: operations
      summary: Execute root recovery after the configured delay
      parameters:
      - $ref: '#/components/parameters/WalletId'
      - $ref: '#/components/parameters/ChainId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties: {}
      responses:
        '202':
          description: Acción aceptada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyActionResult'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets/{walletId}/accounts/{chainId}/recovery/delay:
    post:
      operationId: setRecoveryDelay
      x-required-scope: operations
      summary: Set the delay a scheduled root recovery waits before it can be executed
      description: >-
        The delay is read when a recovery is scheduled, not when it is executed, so a change
        applies to the next scheduled recovery and never to one already pending. The bounds are
        the account contract's own: one hour at the least, thirty days at the most.
      parameters:
      - $ref: '#/components/parameters/WalletId'
      - $ref: '#/components/parameters/ChainId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
              - delaySeconds
              properties:
                delaySeconds:
                  type: integer
                  minimum: 3600
                  maximum: 2592000
      responses:
        '202':
          description: Acción aceptada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyActionResult'
        '400':
          description: >-
            The delay is outside the account contract's bounds (`RECOVERY_DELAY_OUT_OF_RANGE`).
            Refused before any chain call rather than left to revert: a value the contract will
            not accept is a bad request, and answering it with a reverted transaction charges gas
            to say no. `details` carries `requested`, `minimum` and `maximum` in seconds.
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/wallets/{walletId}/accounts/{chainId}/revoke-native-delegation:
    post:
      operationId: revokeNativeDelegation
      x-required-scope: operations
      summary: Revoke an EIP-7702 native delegation by authorizing address(0)
      parameters:
      - $ref: '#/components/parameters/WalletId'
      - $ref: '#/components/parameters/ChainId'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties: {}
      responses:
        '202':
          description: Acción aceptada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyActionResult'
        '409':
          $ref: '#/components/responses/ProblemResponse'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/notification-channels:
    get:
      operationId: listNotificationChannels
      x-required-scope: operations
      summary: List channel adapters and configuration state
      responses:
        '200':
          description: Channel adapters
          content:
            application/json:
              schema:
                type: object
                required:
                - items
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/NotificationChannel'
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
  /v1/internal/notification-deliveries:
    get:
      operationId: listNotificationDeliveries
      x-required-scope: internal
      summary: List internal notification delivery attempts
      x-internal: true
      responses:
        '200':
          description: Delivery attempts
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '401':
          $ref: '#/components/responses/ProblemResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          $ref: '#/components/responses/ProblemResponse'
security:
- bearerAuth: []
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      # Not opaque: the token is signed by the deployment and carries issuer, audience, client,
      # tenant, environment, scopes and the credential version it was issued under (`DEC-013`).
      # Its internal encoding is not part of this contract -- a consumer treats it as a string --
      # but calling it opaque stopped describing what the API issues.
      bearerFormat: signed-client-credentials-token
      description: >-
        A token obtained from `POST /oauth/token`. Authentication is not authorisation: every
        `/v1/**` operation declares the scope it requires in `x-required-scope`, and a client that
        authenticates without holding it is answered `403 INSUFFICIENT_SCOPE`, not `401`.


        Scopes (`GAP-007`):


        * `operations` — everything a consumer calls to move funds or read state.

        * `internal` — the observability routes under `/v1/internal/**`.

        * `clients:manage` — the administrative surface under `/v1/admin/**`, and nothing else. It
          is never implied by any other scope: a client is granted it by name or does not have it.
  parameters:
    ClientId:
      name: clientId
      in: path
      required: true
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 255
    WalletId:
      name: walletId
      in: path
      required: true
      schema:
        type: string
    OperationId:
      name: operationId
      in: path
      required: true
      schema:
        type: string
    ChainId:
      name: chainId
      in: path
      required: true
      schema:
        type: string
        enum: *id001
  responses:
    ProblemResponse:
      description: Stable problem response
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Problem'
    RateLimited:
      description: >-
        The caller is over one of its bounds (`CAP-001` REQ-001-04). Authentication says who is
        calling and nothing about how often, so every authenticated route is bounded per client,
        per tenant and for the deployment as a whole. The refusal happens before the work is
        started, so a request answered this way costs the deployment nothing, and it consumes none
        of the bounds it did not exceed. `code` is `RATE_LIMIT_EXCEEDED` and `details.scope` names
        which bound was reached — `client`, `tenant` or `deployment` — never another caller's
        identifier. Wait `Retry-After` seconds; retrying sooner is refused again and is charged
        nothing.
      headers:
        Retry-After:
          description: Seconds until the exceeded bound has room again.
          required: true
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    DecimalAmount:
      type: string
      pattern: ^[0-9]+(\.[0-9]+)?$
    ExecutionLane:
      type: string
      enum:
      - partner
      - sandbox
      description: >-
        Which set of platform accounts pays for an operation. A deployment may fund the public
        sandbox separately from the Partner's own traffic, so that a visitor exhausting the demo
        leaves the pilot running.

        A deployment that funds one set answers `partner` everywhere, which is what every
        deployment answered before lanes existed. The lane is decided by the authenticated client
        and never by anything a request carries, exactly as the tenant is (`GAP-007`).
    FailureMode:
      type: string
      enum:
      - none
      - transient_once
      - broadcast_unknown_once
      default: none
      description: Development-only deterministic failure injection.
    Health:
      type: object
      description: >-
        Liveness plus the identity of the answering build. `version` and `profile` are required so a
        consumer can assert which API build responded and whether it executes against a real network,
        instead of inferring either from its own environment. The response also carries the full
        pilot overview payload; only the fields a consumer may rely on are declared here.
      required:
      - status
      - service
      - version
      - profile
      - outbox
      properties:
        status:
          type: string
          const: ok
        service:
          type: string
        version:
          type: string
          description: Version of the running API build, as declared by its own package manifest.
          examples:
          - 0.13.0
        profile:
          type: string
          description: >-
            Runtime profile the API is executing. Replaces the retired `chainMode` field. Only
            `testnet` and `production` broadcast to a public network; `mock` fabricates transaction
            hashes and `anvil` executes on a local chain.
          enum:
          - mock
          - anvil
          - testnet
          - production
        outbox:
          type: object
          description: Outbox depth by state. Replaces the retired scalar `outboxPending` field.
          required:
          - pending
          - processing
          - failed
          properties:
            pending:
              type: integer
            processing:
              type: integer
            failed:
              type: integer
        lifiMode:
          type: string
          enum:
          - disabled
          - simulated
          - live-readonly
        database:
          type: object
          properties:
            enabled:
              type: boolean
            status:
              type: string
    TenantUser:
      type: object
      required:
      - userId
      - externalUserId
      - status
      - createdAt
      properties:
        userId:
          type: string
        externalUserId:
          type: string
        status:
          type: string
          const: active
        createdAt:
          type: string
          format: date-time
    RecipientAlias:
      type: object
      additionalProperties: false
      required: [aliasId, userId, type, normalizedValue, status, createdAt]
      properties:
        aliasId:
          type: string
        userId:
          type: string
        type:
          type: string
          enum: [phone]
        normalizedValue:
          type: string
        status:
          type: string
          enum: [resolved, unactivated, pending_claim, blocked]
        createdAt:
          type: string
          format: date-time
    RecipientResolution:
      type: object
      additionalProperties: false
      required: [type, normalizedValue, chainId, status]
      properties:
        type:
          type: string
          enum: [phone]
        normalizedValue:
          type: string
        chainId:
          type: string
          enum: *id001
        status:
          type: string
          enum: [resolved, unactivated, pending_claim, blocked, not_found]
        userId:
          type: string
        walletId:
          type: string
        chainAccountId:
          type: string
        address:
          type: string
        accounts:
          description: >-
            One row per requested network. The EVM rows carry the addresses themselves rather than
            a claim that they match, so an account model that does not hold DEC-016 is visible in
            the answer instead of hidden behind a boolean. The baseline `eip7702` model holds it by
            construction -- an EOA is the same address on every EVM network -- and no execution
            protocol can change that.
          type: array
          items:
            $ref: '#/components/schemas/ResolvedChainAccount'
    WalletAccounts:
      type: object
      additionalProperties: false
      required: [walletId, accounts]
      properties:
        walletId:
          type: string
        accounts:
          description: >-
            One row per active network of the deployment, in catalogue order. Same rows as a
            recipient resolution, because it is the same question asked from the other identifier.
          type: array
          items:
            $ref: '#/components/schemas/ResolvedChainAccount'
    ResolvedChainAccount:
      type: object
      additionalProperties: false
      required: [chainId, family, status, provisioned]
      properties:
        chainId:
          type: string
          enum: *id001
        family:
          type: string
          enum: [evm, bitcoin, cardano]
        status:
          description: >-
            Per-network resolution state. `unavailable` means the network could not answer at all
            (inactive in this deployment, or its adapter could not derive an address without
            writing); it is not the same as an address that exists but is not provisioned yet.
          type: string
          enum: [resolved, unactivated, unavailable]
        provisioned:
          description: Whether a ChainAccount already exists for this wallet on this network.
          type: boolean
        address:
          description: >-
            Present whenever the address is knowable, including before provisioning. Absent only
            when the row says why in `reason`.
          type: string
        addressType:
          description: >-
            The account model this address belongs to. It is the only thing that decides the
            address, and it is fixed for the account's lifetime: changing how a deployment
            executes (`executionModes`) or who pays the gas never changes it (GAP-014). The
            baseline `eip7702` account is the wallet's own EOA, and it is the sender of a
            UserOperation on the ERC-4337 path -- an ERC-4337 wallet does not have a different
            address from an EIP-7702 one. The `*_native`/`*_harness`/`simulated_eoa` values are
            historical spellings still returned for accounts provisioned before the account model
            and the execution protocol were separated. The CREATE2 smart-account model was
            withdrawn on 2026-08-13, before production: it is not offered and cannot be selected.
          type: string
          enum:
            - eip7702
            - simulated
            - bitcoin_p2wpkh
            - cardano_base
            - cardano_enterprise
            - simulated_eoa
            - eip7702_native
        chainAccountId:
          type: string
        reason:
          description: Why this network has no address, when it has none.
          type: string
    ProvisionedRecipient:
      type: object
      additionalProperties: false
      required:
      - type
      - normalizedValue
      - chainId
      - status
      - userId
      - walletId
      - chainAccountId
      - address
      - created
      properties:
        type:
          type: string
          enum: [phone]
        normalizedValue:
          type: string
        chainId:
          type: string
          enum: *id001
        status:
          type: string
          enum: [resolved]
        userId:
          type: string
        walletId:
          type: string
        chainAccountId:
          type: string
        address:
          type: string
        created:
          type: boolean
          description: >-
            True when this call created the recipient or the missing chain account, false when it
            only read back what already existed.
    WalletProfile:
      type: object
      required:
      - walletId
      - userId
      - status
      - createdAt
      properties:
        walletId:
          type: string
        userId:
          type: string
        status:
          type: string
          const: active
        createdAt:
          type: string
          format: date-time
    ChainAccount:
      type: object
      required:
      - chainAccountId
      - walletId
      - address
      - addressType
      - chain
      - status
      - executionModes
      properties:
        chainAccountId:
          type: string
        walletId:
          type: string
        address:
          type: string
        addressType:
          description: >-
            The account model this address belongs to; see `addressType` on the account row. Never
            decided by the execution protocol.
          type: string
          enum:
          - eip7702
          - simulated
          - simulated_eoa
          - eip7702_native
        chain:
          type: string
        status:
          type: string
          const: operational
        executionModes:
          type: array
          items:
            type: string
    DepositRoute:
      type: object
      required:
      - depositRouteId
      - walletId
      - assetId
      - address
      properties:
        depositRouteId:
          type: string
        walletId:
          type: string
        assetId:
          type: string
        address:
          type: string
    DepositObservation:
      type: object
      description: >-
        The on-chain transfer a deposit was credited from, on a deposit this platform observed
        rather than funded itself (`CAP-030` REQ-030-04). It answers the two questions a credit
        raises and an operation record alone cannot: which movement on which chain this is, and why
        the money is not spendable yet when it is not.
      required:
      - chainId
      - transactionHash
      - transferIndex
      - address
      - observedAt
      properties:
        chainId:
          type: string
          enum: *id001
        transactionHash:
          type: string
        transferIndex:
          type: integer
          minimum: 0
          description: >-
            Where inside the transaction this transfer is: the output index on a UTXO family, the
            index of the token transfer's log entry on an account family, and `0` for the value a
            transaction itself moves where the chain gives no index. With `chainId` and
            `transactionHash` it is the whole identity of the transfer, and the only thing a credit
            is idempotent on (`CAP-030` REQ-030-05) — which is why the same transfer, re-read after
            a restart or a backfill, never credits twice.
        address:
          type: string
          description: The deposit-route address the funds arrived at.
        observedAt:
          type: string
          format: date-time
        confirmations:
          type: integer
          description: How deep the transfer was at the last reading.
        finalityThreshold:
          type: integer
          description: >-
            The depth this network requires before the deposit is credited (`CAP-030` REQ-030-07).
            Stated rather than inferred: without it, a deposit that has not been credited yet and
            one that never will be look the same from outside.
        creditedAt:
          type: string
          format: date-time
          description: >-
            When the ledger was credited. Absent while the transfer has not reached the threshold —
            the funds are reported as `pendingIncoming` until then and are never available to spend.
        simulated:
          type: boolean
          description: >-
            True when a simulated observer produced this deposit. A simulated credit and a real one
            are otherwise the same row, and no surface may present one as the other (`CAP-030`
            AC-030-03).
        reversal:
          $ref: '#/components/schemas/DepositReversal'
    DepositReversal:
      type: object
      description: >-
        A credit undone because the chain no longer carries the transfer it came from. A threshold
        is a policy and not a proof, so a transfer credited at that depth can still be dropped by a
        reorganisation. The correction is new postings that bring the ledger back to what the chain
        says (`CAP-030` REQ-030-08) — never a change to the operation that credited it, which stays
        `completed`: the ledger is append-only, and an operation moved backwards after settling
        would erase the fact that the money was ever credited. This is where the whole history is
        readable: credited, then reversed, and why.
      required:
      - reason
      - reversedAt
      - amountBaseUnits
      properties:
        reason:
          type: string
          enum:
          - reorg
          description: >-
            Why the credit was undone. `reorg` is the transfer being absent from the chain the
            deployment now reads, after having been credited from it.
        reversedAt:
          type: string
          format: date-time
        amountBaseUnits:
          type: string
          pattern: ^[0-9]+$
          description: What was taken back, in the asset's base units. Equal to what was credited.
        detectedAtBlockHeight:
          type: integer
          description: The height the deployment was reading when the transfer was found missing.
    UnattributedDeposit:
      type: object
      description: >-
        A transfer that arrived at an address this deployment watches and could not be attributed to
        a wallet and an asset representation it declares (`CAP-030` REQ-030-09, AC-030-02). It
        credits nothing and it is never dropped: everything the chain said about it is kept, so
        somebody can decide what it was. A token outside the deployment's manifest and a Cardano
        native asset (`GAP-021`) both arrive here.
      required:
      - chainId
      - transactionHash
      - transferIndex
      - address
      - amountBaseUnits
      - observedAt
      - reason
      properties:
        chainId:
          type: string
          enum: *id001
        transactionHash:
          type: string
        transferIndex:
          type: integer
          minimum: 0
        address:
          type: string
        walletId:
          type: string
          description: >-
            The wallet that holds the address, when one does. Present and still uncredited is the
            ordinary case: the address is known and the asset is not.
        assetId:
          type: string
          description: >-
            The asset as the chain names it — a contract address, a policy id and asset name, or the
            network's own name for its coin. Not a deployment representation identifier: if it were
            one, this row would have been credited.
        assetSymbol:
          type: string
          description: The ticker the asset declares for itself, where it declares one. Never identity.
        amountBaseUnits:
          type: string
          pattern: ^[0-9]+$
          description: >-
            The amount in the asset's base units. Base units and not decimal units: an asset whose
            decimals nothing declares has no decimal figure, and an amount at the wrong scale is a
            wrong amount.
        observedAt:
          type: string
          format: date-time
        reason:
          type: string
          enum:
          - asset_not_declared
          - address_not_attributable
          - cardano_native_asset
          description: >-
            `asset_not_declared` is a transfer of an asset outside this deployment's manifest.
            `address_not_attributable` is a transfer to a watched address no current wallet claims.
            `cardano_native_asset` is the case `GAP-021` holds open: one Cardano output carries ada
            and any number of native assets under one output index, so the transfer identity of
            REQ-030-05 does not tell them apart, and inventing one now would have to be changed
            later — which would make a re-scan credit everything a second time.
        simulated:
          type: boolean
          description: True when a simulated observer recorded it.
    AssetRepresentation:
      type: object
      required:
      - assetId
      - symbol
      - decimals
      - chainId
      - canonicality
      properties:
        assetId:
          type: string
        symbol:
          type: string
        decimals:
          type: integer
        chainId:
          type: string
          description: >-
            The chain this representation lives on. Named `network` in earlier revisions of this
            contract, which no implementation ever returned.
        canonicality:
          type: string
          description: >-
            Whether this representation is the asset issued by its official issuer on that
            network, or a representation deployed by this solution for testing. Symbols are not
            asset identity: `USDT` on a testnet is not Tether, and only this field says so.
          enum:
          - canonical
          - non-canonical-test
    Balance:
      type: object
      description: >-
        What a wallet holds on one asset of one chain. **`available` is the chain's own figure**
        (`CAP-040` REQ-040-09): a person asking what they hold is asking about the chain, and an
        answer derived from this platform's bookkeeping reports zero for every coin that arrived
        without this platform sending it. The ledger's projection travels beside it in `ledger`.
      required:
      - walletId
      - assetId
      - available
      - reserved
      - pendingIncoming
      - ledger
      - source
      properties:
        walletId:
          type: string
        assetId:
          type: string
          description: >-
            The deployment's representation identifier, or — for an asset the chain reports and this
            deployment does not declare, such as a native asset on Cardano — the chain's own name
            for it. What a person holds is not bounded by what this deployment knows how to issue.
        available:
          $ref: '#/components/schemas/DecimalAmount'
          description: >-
            What the wallet holds, in the asset's units, as the chain reports it. On an asset whose
            decimals nothing declares, the figure is in base units and `decimals` is absent — an
            amount at the wrong scale is a wrong amount.
        reserved:
          $ref: '#/components/schemas/DecimalAmount'
        pendingIncoming:
          $ref: '#/components/schemas/DecimalAmount'
          description: Funds accepted towards this wallet that are not credited yet — an incoming
            transfer whose operation has not reached its accounting step. Never counted in
            `available`.
        ledger:
          $ref: '#/components/schemas/DecimalAmount'
          description: >-
            What this deployment's ledger accounts for the same asset. Equal to `available` on funds
            this platform moved, and smaller on anything that arrived from outside.
        source:
          type: string
          enum:
          - chain
          - ledger
          description: >-
            Where `available` came from. `ledger` means this deployment could not read the chain — no
            provider configured, a read that failed, or a simulated profile — and **never** that the
            address is empty (`AC-040-08`).
        readAt:
          type: string
          format: date-time
          description: When the chain was read, on a row whose `source` is `chain`.
        blockHeight:
          type: integer
          description: The height the reading was made against, when the provider reports one. A
            reading with no height cannot be told apart from a cached one.
        awaitingCredit:
          $ref: '#/components/schemas/AwaitingCredit'
          description: >-
            The part of `available` this platform has yet to credit (`REQ-040-08`). Present only
            where the chain reports more than the ledger accounts for.
    AwaitingCredit:
      type: object
      required:
      - baseUnits
      - amount
      description: >-
        Money the wallet holds that this platform has not credited, so a transfer from here cannot
        spend it yet. Published rather than left to be derived: a consumer that must subtract two
        numbers before it can state a fact will sometimes not state it, and one did — measured on
        2026-08-29, a conversational client holding both figures in the same row answered `0 ADA`
        against 10000 of them.


        It is a waiting state and not a permanent one: incoming funds are credited by the deposit
        observation of `CAP-030` REQ-030-04, once the transfer that carried them is as deep as its
        network requires (REQ-030-07). `reason` says which of the three situations a row is in, so a
        consumer can tell somebody to wait, or to ask for help, rather than only that a number does
        not add up.
      properties:
        baseUnits:
          type: string
          description: The figure in the asset's base units.
          example: '10000000000'
        amount:
          type: string
          description: The same figure in the asset's units, ready to state as it stands.
          example: '10000'
        reason:
          type: string
          description: >-
            Why this part is not credited. `awaiting_finality` means the transfer was seen and is
            not deep enough yet — it credits itself, and the wait is the answer. `not_observed`
            means no transfer accounts for the difference, which is what a wallet funded before this
            deployment watched its route looks like. `not_attributable` means a transfer was seen
            and could not be attributed to an asset this deployment declares (`CAP-030` REQ-030-09)
            — a token outside the manifest, or a Cardano native asset, which `GAP-021` leaves
            uncredited on purpose. Absent when the deployment cannot tell them apart; `mixed` when
            more than one applies to the same row.
          enum:
          - awaiting_finality
          - not_observed
          - not_attributable
          - mixed
        confirmations:
          type: integer
          description: >-
            How deep the transfer is, on a row whose reason is `awaiting_finality`, against
            `finalityThreshold`. The two together are what turns "not yet" into a wait somebody can
            act on.
        finalityThreshold:
          type: integer
          description: The depth this network requires before a deposit is credited here.
    Quote:
      type: object
      required:
      - quoteId
      - walletId
      - assetId
      - amount
      - amountBaseUnits
      - destination
      - expiresAt
      - feeMode
      - fees
      - operationType
      - totalDebit
      - totalDebitBaseUnits
      - settlementProvider
      properties:
        slippage:
          type: number
          description: >-
            The slippage bound this quote was priced under, whether the caller named it or the
            deployment's default applied. Present only on a quote a third-party router priced:
            a quote nobody else priced has no such bound, and stating a default there would
            describe nothing (`CAP-050` AC-050-14).
        quoteSource:
          $ref: '#/components/schemas/QuoteSource'
        routeExecutionMode:
          $ref: '#/components/schemas/RouteExecutionMode'
        simulated:
          deprecated: true
          description: >-
            Answers one question only since GAP-004: whether the provider's route is what moves the
            funds. Read `quoteSource` to know how the price was obtained.
          type: boolean
        quoteId:
          type: string
        walletId:
          type: string
        assetId:
          type: string
          description: The representation the quote is priced and will be executed on, which is
            authoritative and may differ from the one the request named. When the requested
            network cannot fund the amount and another one covers it whole, the quote is issued
            on that network instead of being refused (CAP-050 REQ-050-06). Read this field and
            `chainId` from the quote rather than assuming the request's.
        amount:
          $ref: '#/components/schemas/DecimalAmount'
        amountBaseUnits:
          type: string
          pattern: ^[0-9]+$
        destination:
          type: string
        expiresAt:
          type: string
          format: date-time
        feeMode:
          type: string
          enum:
          - partner_sponsored
          - asset_fee
        fees:
          $ref: '#/components/schemas/FeeBreakdown'
        operationType:
          $ref: '#/components/schemas/OperationType'
        totalDebit:
          $ref: '#/components/schemas/DecimalAmount'
        totalDebitBaseUnits:
          type: string
          pattern: ^[0-9]+$
        settlementProvider:
          type: string
          enum:
          - direct
          - lifi
          - deployment_pool
        destinationKind:
          type: string
          description: What the destination resolved to. A consumer must warn before confirming
            a split toward an external address, which receives several transfers on several
            networks, irreversibly (CAP-050 REQ-050-05).
          enum:
          - platform_account
          - external_address
        requiresSplitDisclosure:
          type: boolean
          description: >-
            Whether the disclosure of `CAP-050` REQ-050-05 is owed on this quote (AC-050-10). True
            only when the destination is external **and** the quote carries more than one leg: a
            single-network send to an external address does not owe it, and neither does a split
            toward an account this platform manages.


            Stated rather than left to be inferred from `destinationKind` and `legs` together. Two
            conditions a consumer has to combine are two a consumer can combine wrongly, and one
            did: a conversational consumer warned that funds would arrive as several transfers on
            several networks on a single-network send, which describes a different send from the
            one being confirmed and reads as a reason not to confirm.
        splitDisclosure:
          type: object
          description: >-
            The disclosure of `CAP-050` REQ-050-05, already written, present exactly when
            `requiresSplitDisclosure` is true (REQ-050-09). A consumer shows `text`; it no longer
            has to compose it. Measured over six runs of the same request on 2026-08-13, a
            conversational consumer that composed the warning named the destination as external
            in four, called the send irreversible in three, and stated the whole warning in one.


            `facts` carries the same statement in parts, for a consumer that renders its own
            wording -- the networks, how many transfers arrive, and that each pays its own fee.
          additionalProperties: false
          required:
          - code
          - text
          - locale
          - facts
          properties:
            code:
              type: string
              enum:
              - split_to_external_address
            text:
              type: string
              minLength: 1
            locale:
              type: string
              description: The locale `text` is written in. The one the quote was asked for when
                this deployment writes it, and the deployment's default otherwise.
            facts:
              type: object
              description: >-
                The parts of the statement that vary, for a consumer that renders its own wording.
                What does not vary is carried by `code`: a send under
                `split_to_external_address` always leaves this platform, always pays a fee per
                transfer, and can never be reversed. A field whose value is the same on every
                quote states nothing, so none is published.
              additionalProperties: false
              required:
              - transferCount
              - networks
              properties:
                transferCount:
                  type: integer
                  minimum: 2
                networks:
                  type: array
                  minItems: 2
                  items:
                    type: string
                    enum: *id001
        legs:
          type: array
          description: Present only on a split quote (CAP-050 REQ-050-04). One same-chain
            transfer leg per source network; the legs' amounts sum to the requested amount. A
            quote without legs is a single transfer.
          minItems: 2
          items:
            $ref: '#/components/schemas/QuoteLeg'
    QuoteLeg:
      type: object
      description: One same-chain piece of a split quote. Each leg leaves from the network its
        balance already sits on, in that network's own asset representation. No leg bridges.
      required:
      - chainId
      - assetId
      - amount
      - amountBaseUnits
      - fees
      - totalDebitBaseUnits
      properties:
        chainId:
          type: string
        assetId:
          type: string
        amount:
          $ref: '#/components/schemas/DecimalAmount'
        amountBaseUnits:
          type: string
          pattern: ^[0-9]+$
        fees:
          $ref: '#/components/schemas/FeeBreakdown'
        totalDebitBaseUnits:
          type: string
          pattern: ^[0-9]+$
    Operation:
      type: object
      required:
      - operationId
      - type
      - status
      - walletId
      - assetId
      - amount
      - amountBaseUnits
      - executionMode
      - createdAt
      - updatedAt
      - attempt
      - totalDebitBaseUnits
      properties:
        quoteSource:
          $ref: '#/components/schemas/QuoteSource'
        routeExecutionMode:
          $ref: '#/components/schemas/RouteExecutionMode'
        simulated:
          deprecated: true
          description: >-
            Answers one question only since GAP-004: whether the provider's route is what moves the
            funds. Read `quoteSource` to know how the price was obtained.
          type: boolean
        operationId:
          type: string
        type:
          type: string
          enum:
          - deposit
          - transfer
          - settlement
        status:
          type: string
          enum:
          - accepted
          - processing
          - retrying
          - broadcast_unknown
          - completed
          - partial
          - failed
          description: partial is the explicit state of a split send with at least one completed
            leg and at least one failed one. It is never presented as completed; the failed legs
            are retryable and only what effectively happened is accounted (CAP-060 REQ-060-08).
        walletId:
          type: string
        assetId:
          type: string
        amount:
          $ref: '#/components/schemas/DecimalAmount'
        amountBaseUnits:
          type: string
          pattern: ^[0-9]+$
        destination:
          type: string
        origin:
          type: string
          description: >-
            Where a deposit's funds came from. Only ever present on `type: deposit`, and only in a
            non-production environment. `test_funding` marks a balance this platform minted for
            itself -- granted automatically when the account was provisioned, or requested through
            the sandbox faucet -- so no surface may present it as a payment from anybody. A deposit
            without this field is a real incoming one: somebody sent those funds to the wallet's
            deposit route and a chain observation credited them (`CAP-030` REQ-030-10), and the
            transfer it was credited from is under `deposit`.
          enum:
          - test_funding
        deposit:
          $ref: '#/components/schemas/DepositObservation'
          description: >-
            The on-chain transfer this deposit was credited from. Present on a `type: deposit`
            operation an observer produced, and absent on one this platform funded itself, which has
            no incoming transfer to point at.
        executionMode:
          deprecated: true
          description: >-
            Deprecated single-enum label for the profile this operation ran under. It says nothing
            about the account's address, which belongs to the account model alone (GAP-014).
          type: string
        transactionHash:
          type: string
          description: >-
            One transaction, on one network. An operation that settled on more than one -- a split
            send, a cross-chain settlement -- reports its first here and the whole set under
            `transactions`. This field is not widened to stand for the set: every consumer written
            against it reads it as a single transaction, which is what it has always been.
        transactionHashes:
          type: array
          description: >-
            Every hash this operation produced, as a flat list that does not say which network each
            one belongs to. Read `transactions` to attribute them; this field is kept for callers
            written against it.
          items:
            type: string
        transactions:
          type: array
          description: >-
            The same transactions, each named with the network it settled on and, where the
            deployment publishes one, its explorer link. An operation can produce more than one:
            a split send makes one transfer per source network and a cross-chain settlement one
            per side, so a Partner reconciling either had to attribute a flat list of hashes by
            guessing. Ordered as `transactionHashes` is. Present once at least one transaction has
            confirmed.
          items:
            type: object
            required:
            - chainId
            - transactionHash
            properties:
              chainId:
                type: string
                enum: *id001
              transactionHash:
                type: string
              explorerUrl:
                type: string
                format: uri
              networkCost:
                $ref: '#/components/schemas/NetworkCost'
                description: >-
                  What this transaction cost on its own network, and which account paid it
                  (REQ-070-12). The operation-level `networkCost` describes one transaction, so it
                  is absent on an operation that produced several: this is where the cost of each
                  one is read, whichever of the three shapes the operation has. Absent on a
                  transaction the platform did not pay for — the destination side of a cross-chain
                  settlement, which another party broadcasts.
        explorerUrl:
          type: string
          format: uri
          description: >-
            The link for `transactionHash`, and so for one network only. A split send publishes
            one per network under `transactions` instead.
        error:
          type: string
        attempt:
          type: integer
          minimum: 0
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        operationType:
          $ref: '#/components/schemas/OperationType'
        totalDebitBaseUnits:
          type: string
          pattern: ^[0-9]+$
        lane:
          $ref: '#/components/schemas/ExecutionLane'
          description: >-
            Which set of platform accounts paid for this operation. Recorded on the operation
            because the worker that transmits runs long after the client that decided the lane is
            gone, and because it is what says whose money the sponsored gas was: an operation of
            the `sandbox` lane spent the accounts funded for the public demo, and one of the
            `partner` lane spent the Partner's.

            Absent means `partner`, which is what every operation created before lanes existed
            spent and what a deployment funding a single lane keeps spending.
        fees:
          $ref: '#/components/schemas/FeeBreakdown'
        networkCost:
          $ref: '#/components/schemas/NetworkCost'
          description: >-
            What one transaction cost. Absent on an operation that produced several — a split send,
            a cross-chain settlement — because a single figure names one gas amount, one price and
            one payer, and those operations have one of each per network. Read `networkCostTotal`
            for what the operation cost, and `transactions[].networkCost` for the breakdown.
        networkCostTotal:
          $ref: '#/components/schemas/NetworkCostTotal'
        reconciliation:
          $ref: '#/components/schemas/OperationReconciliation'
    NetworkCost:
      type: object
      description: |-
        What the operation cost on chain and which account paid it. Distinct from
        `FeeBreakdown.networkFee`, which is what the *user* is charged in the transferred asset and
        is legitimately zero under a sponsored policy. This is what the platform spent.

        Two shapes, discriminated by `measured`, because "it cost nothing" and "nobody measured it"
        are different claims: an operation used to report `networkFee: "0"` with
        `networkFeeSponsored: true` while the relayer was spending real ETH.
      required:
      - measured
      properties:
        measured:
          type: boolean
        totalNativeBaseUnits:
          type: string
          pattern: ^[0-9]+$
          description: Present when `measured` is true. Smallest native unit of the chain — wei on
            EVM, satoshi on Bitcoin, lovelace on Cardano.
        paidBy:
          type: string
          enum:
          - paymaster
          - relayer
          - user
          description: Present when `measured` is true. On Bitcoin and Cardano the fee comes out of
            the sender's own inputs, which is why `user` exists and why nothing is posted as
            sponsorship for it.
        paidByAddress:
          type: string
        gasUsed:
          type: string
          pattern: ^[0-9]+$
          description: Decomposition of the total on a gas-metered chain. Absent where gas is not
            the unit of account.
        effectiveGasPriceWei:
          type: string
          pattern: ^[0-9]+$
        native:
          type: object
          additionalProperties: false
          description: >-
            Present when `measured` is true. The same amount as `totalNativeBaseUnits`, stated in
            the coin's own display units and naming the coin (AC-070-15).

            Both readings are published because they answer different questions: the base-unit
            integer is what reconciliation compares against a balance, and this is what a person
            asked "what did this cost" can read. `networkCostTotal.native` has carried the
            operation's total this way since 2026-08-23 and each transaction did not, so a
            consumer asking for the breakdown of a split send found the whole in a readable unit
            and the parts only in wei.

            The scale is the chain's own — wei is 18, satoshi 8, lovelace 6 — so `total` is never
            the base-unit figure divided by an assumed power of ten.
          required:
          - total
          - symbol
          properties:
            total:
              $ref: '#/components/schemas/DecimalAmount'
            symbol:
              type: string
              description: The coin the chain charges in, as the catalog spells it.
        usd:
          $ref: '#/components/schemas/UsdValue'
          description: >-
            `totalNativeBaseUnits` valued in USD at the rate this deployment held when the cost was
            measured, so a reader can add a cost paid in wei to one paid in lovelace and to a fee
            charged in USDC. Frozen with the measurement, so the same operation reports the same
            cost every time it is asked.

            Present when `measured` is true and the deployment holds a rate for the chain's native
            asset. A deployment that holds none reports the native figure alone. Note for
            consumers: that absence states what the deployment could price, and the cost it
            accompanies is the measured one.
        reason:
          type: string
          enum:
          - simulated_profile
          - simulated_provider_execution
          - not_available_from_adapter
          description: Present when `measured` is false. Why there is no figure, so it is never
            read as free.
    NetworkCostTotal:
      type: object
      additionalProperties: false
      description: |-
        What every transaction of one operation cost, added up (REQ-070-12).

        An operation is not one transaction. A split send makes one transfer per source network and
        a cross-chain settlement one per side, so `networkCost` — which names one gas amount, one
        price and one payer — is absent on exactly the operations that burned the most gas, and a
        consumer reading it found nothing where real money was spent. This is the figure whoever
        asks "what did this cost" reads, in every case: an operation with one transaction reports
        the same amount here as in `networkCost`.

        The counts are part of the answer. A total that covered three transactions and was built
        from two measurements is a different claim from one built from three, and stating them is
        what makes the figure checkable against what ran (AC-070-14, AC-070-15).
      required:
      - transactions
      - measured
      properties:
        transactions:
          type: integer
          minimum: 0
          description: How many transactions this total covers.
        measured:
          type: integer
          minimum: 0
          description: >-
            How many of them reported a measurement. The rest are counted in `unmeasured`, so
            `measured` plus every count there equals `transactions`.
        usd:
          $ref: '#/components/schemas/UsdValue'
          description: >-
            The measured figures added in USD, carrying the rate reading of the last transaction
            the sum was built from. Absent when any measured transaction had no USD figure of its
            own: a total missing one of its parts is a smaller number, not a partial one.
        native:
          type: object
          additionalProperties: false
          description: >-
            The measured figures added in the chain's own native coin. Present only when every
            measured transaction was charged in the same coin — wei and lovelace do not add — which
            is why the coin is named alongside the figure.

            Both units are published: `totalBaseUnits` is what reconciliation compares against a
            balance, and `total` is the same amount in the coin's display units, because a
            base-unit integer is not an answer to "how much did this cost".
          required:
          - total
          - totalBaseUnits
          - symbol
          properties:
            total:
              $ref: '#/components/schemas/DecimalAmount'
            totalBaseUnits:
              type: string
              pattern: ^[0-9]+$
            symbol:
              type: string
        unmeasured:
          type: object
          description: >-
            The transactions carrying no measurement, counted by the reason given, so an absent
            figure is never read as a transaction that was free. `not_reported` counts a transaction
            this platform did not pay for and therefore never measured.
          additionalProperties:
            type: integer
            minimum: 1
    PlatformAccount:
      type: object
      description: One account the platform funds so a user does not pay gas.
      required:
      - lane
      - role
      - address
      properties:
        lane:
          $ref: '#/components/schemas/ExecutionLane'
          description: >-
            Which set of platform accounts this one belongs to. Where a deployment funds both, two
            rows on the same chain carry the same `role` and different addresses, and an operator
            reading "the relayer is empty" has to be told which one.

            `fee_collection` is always `partner`: revenue is the Partner's whichever lane paid the
            gas that produced it, and splitting it by lane would invent a second set of books
            nobody keeps.
        role:
          type: string
          enum:
          - relayer
          - operational_validator
          - paymaster
          - entry_point
          - faucet
          - fee_collection
          - funding_wallet
          description: >-
            `faucet` is the account that signs test-balance mints, separate from the relayer since
            `GAP-012` so a backlog of grants cannot delay a transfer or spend what transfers need.
            It is reported wherever it is configured; a deployment without one funds nothing and
            says so at the faucet request itself.

            `fee_collection` is the revenue side of the same report (`DEC-041`): where a Partner's
            collected fees are swept to. It is configuration of the Partner rather than of the
            deployment, is rotatable without a deploy, and is reported here because an account
            somebody has to watch is an account the platform has to report. One entry per Partner
            per chain, resolved as a sweep resolves it.

            `funding_wallet` is the wallet a deployment sends from on a family that mints nothing
            and is funded from outside it: Bitcoin (`REQ-220-12`) and Cardano (`GAP-011`). It is a
            wallet of the deployment rather than a role key, so it carries `walletId` — what an
            operator configures — and, like the relayer, it has no automatic remediation.
        address:
          type: string
        walletId:
          type: string
          description: >-
            The wallet this account belongs to, on a family where the account is a wallet of the
            deployment rather than a role key (`REQ-220-12`). Only `funding_wallet` carries one.
        nativeBalanceWei:
          type: string
          pattern: ^[0-9]+$
          description: >-
            Native balance held at the address, in wei. Present on a chain that counts in wei.

            An account of a family that counts in something else answers `nativeBalanceBaseUnits`
            with `nativeUnit` instead: writing satoshi or lovelace into a field whose name says wei
            is the unit error `REQ-070-11` forbids, and a reader summing the column would be adding
            three different things. An address the chain will not answer for carries neither figure,
            rather than the zero it does not hold.
        nativeBalanceBaseUnits:
          type: string
          pattern: ^[0-9]+$
          description: >-
            Native balance in the family's own base unit — satoshi, lovelace — for an account whose
            chain does not count in wei. Always accompanied by `nativeUnit`.
        nativeUnit:
          type: string
          description: >-
            The base unit `nativeBalanceBaseUnits` and `lowWatermarkBaseUnits` are counted in:
            `sats`, `lovelace`. Named rather than inferred from the chain, so a reader never has to
            know the family to read the number.
        nativeDecimals:
          type: integer
          description: >-
            Decimals between the base unit and the unit a person says out loud, so a caller renders
            0.02652801 BTC from 2652801 satoshi without a table of its own.
        entryPointDepositWei:
          type: string
          pattern: ^[0-9]+$
          description: What the EntryPoint holds for this account — only the Paymaster has one,
            and it, not the contract's own balance, is what pays for sponsored gas.
        entryPointStakeWei:
          type: string
          pattern: ^[0-9]+$
        entryPointStaked:
          type: boolean
        entryPointUnstakeDelaySec:
          type: integer
        lowWatermarkWei:
          type: string
          pattern: ^[0-9]+$
          description: >-
            Balance in wei below which this account is reported as running low. The same number the
            maintenance loop acts on, not a second threshold for display. An account whose balance
            is reported in a base unit carries `lowWatermarkBaseUnits` instead, for the reason
            `nativeBalanceWei` states.
        lowWatermarkBaseUnits:
          type: string
          pattern: ^[0-9]+$
          description: >-
            The same threshold in the family's own base unit, counted in `nativeUnit`, for an
            account whose chain does not count in wei. The minimum is compared against
            `nativeBalanceBaseUnits`, so the two are always in the same unit.
        minimumSource:
          type: string
          enum:
          - deployment_policy
          - protocol
          - none
          description: >-
            Where the threshold above came from, when it came from anywhere. `GAP-016` requires an
            operating margin to be a configurable ChatterPay or deployment policy and to never be
            presented as a rule of the chain, so a reader is told which it is next to the number.
            `none` says nobody decided one, which is a state and not a zero: an account with no
            minimum is unchecked rather than healthy.
        belowLowWatermark:
          type: boolean
          description: True when the figure that matters for this role — the deposit for a
            Paymaster, the native balance for a relayer — is under the threshold this account
            reports.
        partnerId:
          type: string
          description: Which Partner a `fee_collection` account belongs to. Every other role is the
            deployment's own, and carries no `partnerId`.
        accrued:
          type: array
          description: >-
            What has accrued to a `fee_collection` account and has not been swept onto its address
            yet. Per asset, because a collection account collects assets rather than gas, and
            reported as *unswept* because the gap between what the ledger booked and what the
            address holds is exactly what a sweep closes (`DEC-041`). Those two figures disagreeing
            is the reconciliation signal, so neither one stands for the other.
          items:
            type: object
            required:
            - assetId
            - symbol
            - amount
            - amountBaseUnits
            properties:
              assetId:
                type: string
              symbol:
                type: string
              amount:
                $ref: '#/components/schemas/DecimalAmount'
              amountBaseUnits:
                type: string
                pattern: ^[0-9]+$
    PlatformAccountsSnapshot:
      type: object
      description: The platform's funding accounts on one chain, read from the chain at call time.
      required:
      - chainId
      properties:
        chainId:
          type: string
        executionMode:
          deprecated: true
          description: >-
            Deprecated single-enum label for this chain's profile; read `addressType` for the
            account and `executionModes` for the transports.
          type: string
        readAt:
          type: string
          format: date-time
        accounts:
          type: array
          items:
            $ref: '#/components/schemas/PlatformAccount'
        fundingAccountsUnavailable:
          type: string
          description: >-
            Why the deployment's own funding accounts are absent from `accounts`, when they are. A
            chain whose adapter cannot read a relayer can still report where its fees are collected,
            so the revenue side does not disappear with the cost side (`DEC-041`).
        available:
          type: boolean
          description: Present and `false` when this chain has no readable funding accounts — a
            simulated adapter, or an RPC that could not be reached. Stated rather than omitted so
            a chain with nothing configured is distinguishable from one nobody asked about.
        reason:
          type: string
    ChainContractDeployment:
      type: object
      description: >-
        The contracts one chain is running under, as loaded by the API that answers this call.
        Addresses here are the addresses the API executes against, which is what makes them
        verifiable: an integrator can open any of them on that chain's explorer and be looking at
        the deployment their transfers actually used.
      required:
      - chainId
      properties:
        chainId:
          type: string
        numericChainId:
          type: integer
          description: The EVM chain id of the deployment, as recorded in its own manifest. Present
            only for EVM chains.
        executionMode:
          deprecated: true
          type: string
          description: >-
            Deprecated single-enum label for this chain's profile. It conflates the account model,
            the execution protocol and the gas sponsorship, and an earlier revision of this
            document stated that the active value decides how an account address is derived. It
            does not, and it must not: the address belongs to the account model alone
            (`addressType`), and it is fixed for the account's lifetime. Read `addressType` for
            the account and `executionModes` for the transports (GAP-001, GAP-014).
        accountModel:
          description: >-
            What kind of account represents a wallet on this chain. **This alone decides an
            address**, and it is fixed for an account's lifetime. EIP-7702 is the only EVM account
            model: the account is the wallet's own EOA, and that same EOA is the `sender` of a
            UserOperation on the ERC-4337 path (GAP-001, GAP-014). On Cardano, `cardano_base` is a
            payment credential together with a staking one (CIP-19 type 0), which is what makes the
            address one its holder can delegate from; `cardano_enterprise` carries the payment
            credential alone and can never delegate. The two are different addresses of the same
            key material, so an account keeps the model it was provisioned with.
          type: string
          enum: [eip7702, simulated, bitcoin_p2wpkh, cardano_base, cardano_enterprise]
        executionProtocol:
          description: >-
            How this deployment currently sends an operation to the chain. A transport: changing it
            never changes an account's address, which is what makes `direct_relayer` a safe
            fallback for a deployment with funded wallets.
          type: string
          enum: [erc4337, direct_relayer, mock, bitcoin_direct, cardano_direct]
        gasSponsorship:
          description: >-
            Who pays the network fee on this deployment's default path. Distinct from the business
            `feeMode` of a quote: `partner_sponsored` says who bears the cost for the user, this
            says which account actually funds the transaction.
          type: string
          enum: [paymaster, relayer, user]
        executionModes:
          type: array
          description: >-
            Execution protocols this deployment can drive its accounts with, in the order it
            prefers them. They are transports, not identities: every protocol listed here reaches
            the same account at the same address, so a consumer must never read this field as
            saying anything about where a wallet's funds are (GAP-014).
          items:
            type: string
            enum: [erc4337, direct_relayer, mock, bitcoin_direct, cardano_direct]
        entryPointVersion:
          type: string
        generatedAt:
          type: string
          format: date-time
          description: When the deployment manifest was produced. A deployment is fixed for an
            environment until this changes.
        contracts:
          type: array
          items:
            $ref: '#/components/schemas/DeployedContract'
        assets:
          type: array
          items:
            $ref: '#/components/schemas/DeployedAssetContract'
        available:
          type: boolean
          description: Present and `false` when this chain has no contract deployment in this
            environment — a non-EVM chain, or one whose adapter is not configured. Stated rather
            than omitted, so "no deployment here" is distinguishable from "nobody asked".
        reason:
          type: string
    DeployedContract:
      type: object
      description: One contract of a deployment, named by the role it plays rather than by its
        Solidity type, because the role is what an integrator is looking for.
      required:
      - name
      - address
      properties:
        name:
          type: string
          description: Role of the contract in the deployment, e.g. `implementation`,
            `transferModule`, `entryPoint`, `paymaster`, `factory`, `proxyRuntime`.
          enum:
          - implementation
          - transferModule
          - entryPoint
          - paymaster
          - factory
          - proxyRuntime
        address:
          type: string
    DeployedAssetContract:
      type: object
      description: One asset representation of a deployment, with the token contract that backs it.
      required:
      - assetId
      - address
      properties:
        assetId:
          type: string
        address:
          type: string
        symbol:
          type: string
        decimals:
          type: integer
        canonicality:
          type: string
          description: Whether this representation is the network's canonical asset or a test-only
            one this deployment issues. A test asset presented as canonical is the confusion this
            field exists to prevent.
    OperationReconciliation:
      type: object
      description: |-
        Independent verification of a completed operation against external chain evidence.
        Distinct from `Operation.status`: status answers "did the operation run", reconciliation
        answers "does the ledger still agree with the chain" and can be set after the operation is
        already `completed`.
      required:
      - status
      - expectedAmountBaseUnits
      - checkedAt
      properties:
        status:
          type: string
          enum:
          - reconciled
          - discrepancy
          - manual_review
        reason:
          type: string
        expectedAmountBaseUnits:
          type: string
          pattern: ^[0-9]+$
        onchainAmountBaseUnits:
          type: string
          pattern: ^[0-9]+$
        checkedAt:
          type: string
          format: date-time
    OperationStep:
      type: object
      required:
      - stepId
      - operationId
      - name
      - status
      - attempt
      - startedAt
      properties:
        stepId:
          type: string
        operationId:
          type: string
        name:
          type: string
          enum:
          - validate
          - risk
          - reserve
          - build_intent
          - sign
          - simulate
          - submit
          - observe
          - account
          - notify
        status:
          type: string
          enum:
          - started
          - completed
          - failed
          - waiting
        attempt:
          type: integer
          minimum: 1
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
        detail:
          type: object
          additionalProperties: true
    OperationLeg:
      type: object
      required:
      - legId
      - operationId
      - kind
      - sequence
      - chainId
      - status
      - attempt
      - createdAt
      - updatedAt
      properties:
        legId:
          type: string
        operationId:
          type: string
        kind:
          type: string
          enum:
          - approval
          - source_swap
          - bridge
          - destination_swap
          - final_transfer
          - split_transfer
        sequence:
          type: integer
          minimum: 1
        chainId:
          type: string
        status:
          type: string
          enum:
          - pending
          - submitted
          - confirmed
          - failed
          - refund_pending
          - refunded
          - manual_review
        providerRouteId:
          type: string
        amountBaseUnits:
          type: string
        transactionHash:
          type: string
        attempt:
          type: integer
          minimum: 1
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        detail:
          type: object
          additionalProperties: true
    EventEnvelope:
      type: object
      required:
      - eventId
      - eventType
      - eventVersion
      - resourceId
      - occurredAt
      - data
      - sequence
      properties:
        eventId:
          type: string
        eventType:
          type: string
        eventVersion:
          type: integer
        resourceId:
          type: string
        occurredAt:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: true
        sequence:
          type: integer
    EventPage:
      type: object
      required:
      - items
      - nextCursor
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/EventEnvelope'
        nextCursor:
          type: integer
    ReconciliationReportGroup:
      description: >-
        One (chain, asset, outcome) group of reconciliation records. The amounts are sums over the
        group, in the asset's base units, so a discrepancy group states how much money the finding
        is about rather than only how many findings there are.
      type: object
      required: [chainId, assetId, status, count, expectedAmountBaseUnits, onchainAmountBaseUnits]
      properties:
        partnerId:
          type: string
          description: Absent on records written before a tenant was recorded on them.
        chainId:
          type: string
        assetId:
          type: string
        status:
          type: string
          enum:
          - reconciled
          - discrepancy
          - manual_review
        count:
          type: integer
        expectedAmountBaseUnits:
          type: string
        onchainAmountBaseUnits:
          type: string
    WorkerState:
      type: object
      required:
      - workerId
      - status
      - processed
      - failed
      - heartbeatAt
      properties:
        workerId:
          type: string
        status:
          type: string
          enum:
          - idle
          - processing
          - stopped
        currentOutboxId:
          type: string
        processed:
          type: integer
        failed:
          type: integer
        heartbeatAt:
          type: string
          format: date-time
    ApiClient:
      description: >-
        A registered consuming application. Carries no secret and no secret digest: there is no
        contract, anywhere, that discloses either after creation.
      type: object
      required: [clientId, label, tenantId, environment, application, scopes, status, credentialVersion, createdAt]
      properties:
        clientId:
          type: string
        label:
          type: string
        tenantId:
          type: string
        environment:
          type: string
        application:
          type: string
        scopes:
          type: array
          items:
            type: string
            enum: ['*', operations, internal, clients:manage]
        status:
          type: string
          enum: [active, disabled, revoked]
        credentialVersion:
          description: >-
            Incremented by every rotation and status change. Access tokens carry the version they
            were issued under, which is what makes revocation immediate without a token store.
          type: integer
          minimum: 1
        createdAt:
          type: string
          format: date-time
        rotatedAt:
          type: string
          format: date-time
        revokedAt:
          type: string
          format: date-time
    ProvisionedApiClient:
      description: A client together with the single disclosure of its secret.
      type: object
      required: [client, clientSecret, secretVisibility]
      properties:
        client:
          $ref: '#/components/schemas/ApiClient'
        clientSecret:
          description: >-
            At least 256 bits of entropy, base64url. Shown in this response and never again; the
            API stores an irreversible digest of it.
          type: string
        secretVisibility:
          type: string
          const: once
    Problem:
      type: object
      required:
      - type
      - title
      - status
      - code
      - detail
      - correlationId
      - retryable
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        code:
          type: string
        detail:
          type: string
        correlationId:
          type: string
        retryable:
          type: boolean
    Chain:
      type: object
      required:
      - chainId
      - executionMode
      properties:
        chainId:
          type: string
          enum: *id001
        executionMode:
          deprecated: true
          description: >-
            Deprecated single-enum label; see `executionMode` on the chain row. Never read it as
            saying anything about an account's address.
          type: string
          enum:
          - mock
          - eip7702_harness
          - direct_relayer
          - erc4337
          - eip7702_native
          - bitcoin_direct
          # Added when Cardano stopped being simulated. The vocabulary is closed for EVM, where a
          # single value would conflate an account model with a transport it is free to change
          # (GAP-014); a non-EVM family has one value per dimension and no combination to choose,
          # so the alternative here was reporting a Cardano operation as `mock` -- which would say
          # the opposite of what happened.
          - cardano_direct
    LifiQuoteRequest:
      type: object
      required:
      - fromChainId
      - toChainId
      - fromToken
      - toToken
      - fromAmount
      - fromAddress
      - toAddress
      properties:
        fromChainId:
          type: string
          enum: *id001
        toChainId:
          type: string
          enum: *id001
        fromToken:
          type: string
        toToken:
          type: string
        fromAmount:
          type: string
          pattern: ^[0-9]+$
        fromAddress:
          type: string
        toAddress:
          type: string
        slippage:
          type: number
          minimum: 0
          maximum: 0.5
    QuoteSource:
      description: >-
        How a provider quote's price was obtained (GAP-004). One of two independent dimensions: a
        real price and a real settlement are different claims, and reading one as the other is what
        this separation prevents. `live_provider` means the aggregator answered over the network for
        the canonical tokens it recognises; `simulated` means this deployment computed the numbers
        and no provider was asked.
      type: string
      enum: [live_provider, simulated]
    RouteExecutionMode:
      description: >-
        How a routed operation's funds move (GAP-004). On a quote it is what will happen if the
        quote is accepted, so a caller knows before executing; on an operation it is what happened.
        `live_provider`: the provider's own transaction was broadcast and settled the route.
        `substituted`: the ledger recorded the movement and no provider route was broadcast, so
        there is no cross-chain transaction to look up on an explorer. `simulated`: nothing moved
        anywhere and nothing was recorded as having moved.
      type: string
      enum: [live_provider, substituted, simulated]
    LifiQuote:
      type: object
      required:
      - providerQuoteId
      - quoteSource
      - routeExecutionMode
      - simulated
      - fromChainId
      - toChainId
      - fromAmount
      - toAmountMin
      - tool
      - expiresAt
      properties:
        providerQuoteId:
          type: string
        quoteSource:
          $ref: '#/components/schemas/QuoteSource'
        routeExecutionMode:
          $ref: '#/components/schemas/RouteExecutionMode'
        simulated:
          deprecated: true
          description: >-
            Answers one question only since GAP-004: whether the provider's route is what moves the
            funds. It says nothing about how the price was obtained -- read `quoteSource` for that.
            Derived from `routeExecutionMode` and never set independently.
          type: boolean
        providerAssets:
          description: >-
            The provider-side token identities this quote was priced against (DEC-037, GAP-004).
            The wallet holds a ChatterPay asset representation and the aggregator only recognises
            the canonical token of that network; both identities are real and they are not the same
            thing, so the addresses actually sent to the provider are reported rather than
            substituted silently for the representation id.
          type: object
          additionalProperties: false
          properties:
            fromToken:
              type: string
            toToken:
              type: string
        fromChainId:
          type: string
        toChainId:
          type: string
        fromAmount:
          type: string
        toAmount:
          type: string
        toAmountMin:
          type: string
        tool:
          type: string
        expiresAt:
          type: string
          format: date-time
        transactionRequest:
          type: object
          additionalProperties: true
    LifiStatus:
      type: object
      additionalProperties: true
      properties:
        status:
          type: string
        substatus:
          type: string
        quoteSource:
          $ref: '#/components/schemas/QuoteSource'
        routeExecutionMode:
          $ref: '#/components/schemas/RouteExecutionMode'
        simulated:
          deprecated: true
          type: boolean
    OperationType:
      type: string
      enum:
      - direct_transfer
      - swap
      - cross_chain_transfer
      - polymarket
      - aave
      - nft
    ProviderFeeSource:
      type: string
      enum:
      - lifi
      - swap_provider
      - bridge_provider
      - polymarket
    FeePolicy:
      type: object
      additionalProperties: false
      required:
      - policyId
      - operationType
      - serviceFeeBps
      - providerFeeMode
      - providerFeeSources
      - networkFeeMode
      properties:
        policyId:
          type: string
        operationType:
          $ref: '#/components/schemas/OperationType'
        serviceFeeBps:
          type: integer
          minimum: 0
        providerFeeMode:
          type: string
          enum:
          - none
          - pass_through
          - sponsored
        providerFeeSources:
          type: array
          items:
            $ref: '#/components/schemas/ProviderFeeSource'
        networkFeeMode:
          type: string
          enum:
          - partner_sponsored
          - user_paid
    FeeBreakdown:
      type: object
      additionalProperties: false
      required:
      - policyId
      - operationType
      - feeMode
      - assetId
      - serviceFeeBps
      - serviceFee
      - serviceFeeBaseUnits
      - providerFee
      - providerFeeBaseUnits
      - providerFeeMode
      - providerFeeSources
      - networkFee
      - networkFeeBaseUnits
      - networkFeeSponsored
      - totalFee
      - totalFeeBaseUnits
      - totalDebit
      - totalDebitBaseUnits
      properties:
        policyId:
          type: string
        policyVersion:
          type: integer
          minimum: 1
          description: >-
            Which revision of `policyId` priced this quote. A policy that changes leaves the
            operations quoted under the previous one explainable by the terms they were quoted
            under, which naming the policy alone does not do (`DEC-041`).
        operationType:
          $ref: '#/components/schemas/OperationType'
        feeMode:
          type: string
          enum:
          - partner_sponsored
          - asset_fee
        assetId:
          type: string
        serviceFeeBps:
          type: integer
          minimum: 0
        serviceFee:
          $ref: '#/components/schemas/DecimalAmount'
        serviceFeeBaseUnits:
          type: string
          pattern: ^[0-9]+$
        serviceFeeBoundApplied:
          type: boolean
          description: >-
            States that the policy's floor or ceiling replaced the raw calculation, so a fee that
            does not match `serviceFeeBps` against the amount is explained by the terms rather
            than read as an arithmetic error.
        providerFee:
          $ref: '#/components/schemas/DecimalAmount'
        providerFeeBaseUnits:
          type: string
          pattern: ^[0-9]+$
        providerFeeMode:
          type: string
          enum:
          - none
          - pass_through
          - sponsored
        providerFeeSources:
          type: array
          items:
            $ref: '#/components/schemas/ProviderFeeSource'
        networkFee:
          $ref: '#/components/schemas/DecimalAmount'
        networkFeeBaseUnits:
          type: string
          pattern: ^[0-9]+$
        networkFeeSponsored:
          type: boolean
        totalFee:
          $ref: '#/components/schemas/DecimalAmount'
        totalFeeBaseUnits:
          type: string
          pattern: ^[0-9]+$
        totalDebit:
          $ref: '#/components/schemas/DecimalAmount'
        totalDebitBaseUnits:
          type: string
          pattern: ^[0-9]+$
        serviceFeeMode:
          type: string
          enum:
          - bps
          - fixed_usd
          description: >-
            How the service fee was expressed (`DEC-041`). `bps` is proportional to the amount and
            is the mode of the reference catalogue every deployment falls back to. `fixed_usd` is a
            commercial amount negotiated in money and converted to this asset when the quote was
            priced.
        serviceFeeUsd:
          $ref: '#/components/schemas/DecimalAmount'
          description: The USD amount a `fixed_usd` policy charges, before conversion. Present under
            that mode alone.
        price:
          $ref: '#/components/schemas/QuotedPrice'
          description: >-
            The rate that converted `serviceFeeUsd` into this asset, frozen when the quote was
            issued. The execution charges the frozen rate and never a re-read one, so a disputed
            fee traces to one reading. Present under `fixed_usd` alone.
        borneByUser:
          $ref: '#/components/schemas/DecimalAmount'
          description: >-
            What the payer is charged, stated apart from what the Partner absorbed. The fields
            above collapse the two: a provider fee the policy sponsors and one the user pays both
            land in `providerFee`, and they are different money — one comes out of the balance
            being moved, the other out of the Partner's (`DEC-041`).

            What the operation cost on chain becomes known when it executes, and is reported as
            `Operation.networkCost` with the account that paid it.
        borneByUserBaseUnits:
          type: string
          pattern: ^[0-9]+$
        borneByPartner:
          $ref: '#/components/schemas/DecimalAmount'
          description: What the Partner absorbed, in the asset being moved.
        borneByPartnerBaseUnits:
          type: string
          pattern: ^[0-9]+$
        borneByUserUsd:
          $ref: '#/components/schemas/UsdValue'
          description: >-
            `borneByUser` valued in USD, so what a person paid in fees can be added to what the gas
            of the same operation cost. Present when the deployment holds a rate for this asset; a
            deployment that holds none reports the asset figure alone.
        borneByPartnerUsd:
          $ref: '#/components/schemas/UsdValue'
          description: '`borneByPartner` valued in USD, on the same terms as `borneByUserUsd`.'
    QuotedPrice:
      type: object
      additionalProperties: false
      description: >-
        A rate reading, named and dated. A figure in USD that did not say where the rate came from
        and when it was observed could not be disputed, reproduced or audited (`DEC-041`).
      required:
      - usdPerUnit
      - source
      - readAt
      properties:
        usdPerUnit:
          $ref: '#/components/schemas/DecimalAmount'
          description: USD per one display unit of the asset.
        source:
          type: string
          description: Which source answered, so a disputed figure traces to a reading.
        readAt:
          type: string
          format: date-time
          description: When that source last observed the rate.
    UsdValue:
      type: object
      additionalProperties: false
      description: >-
        An amount in USD together with the rate that produced it. The unit exists so the three
        currencies a single operation spends — the chain's native coin for gas, the asset moved for
        fees, and the money a commercial fee is negotiated in — can be added at all.

        Wherever it appears it is optional, and its absence states which assets the deployment
        holds a rate for. Note for consumers: the figure it accompanies stands on its own, in the
        unit that figure is denominated in.
      required:
      - amount
      - price
      properties:
        amount:
          $ref: '#/components/schemas/DecimalAmount'
        price:
          $ref: '#/components/schemas/QuotedPrice'
    NotificationTarget:
      type: object
      additionalProperties: false
      required:
      - channel
      - recipient
      properties:
        channel:
          description: >-
            The surface the person is actually on (GAP-008). Not the transport and not the adapter
            that delivers: WhatsApp, the portal chat widget and the dev console are all served by
            the same Chatizalo endpoint, and posting to one URL does not make a portal conversation
            a WhatsApp one. `websocket` is withdrawn — it named a transport, not a surface — and is
            still accepted on input, read as `web`, with the substitution logged.
          type: string
          enum:
          - whatsapp
          - telegram
          - instagram
          - web
          - console
          - app
        provider:
          description: >-
            Who delivers to that surface (GAP-008). Optional: absent, the deployment resolves it
            from the channel. `chatizalo` fronts WhatsApp, the widget and the console alike.
          type: string
          enum:
          - chatizalo
          - console
        recipient:
          type: string
          minLength: 1
        locale:
          type: string
    KeyActionResult:
      type: object
      additionalProperties: false
      required:
      - action
      - chainId
      - accountAddress
      - transactionHash
      properties:
        action:
          type: string
          enum:
          - rotate_validator
          - schedule_root_recovery
          - cancel_root_recovery
          - execute_root_recovery
          - set_recovery_delay
          - revoke_native_delegation
        chainId:
          type: string
          enum: *id001
        accountAddress:
          type: string
        transactionHash:
          type: string
        explorerUrl:
          type: string
        executeAfter:
          type: string
          format: date-time
        keyVersion:
          type: integer
          minimum: 1
        recoveryDelaySeconds:
          type: integer
          minimum: 3600
          maximum: 2592000
    NotificationChannel:
      type: object
      required:
      - channel
      - provider
      - implemented
      - configured
      properties:
        channel:
          type: string
          enum:
          - whatsapp
          - telegram
          - instagram
          - web
          - console
          - app
        provider:
          type: string
          enum:
          - chatizalo
          - console
        implemented:
          type: boolean
        configured:
          type: boolean
