openapi: 3.1.0
info:
  title: Sluzen Public Rail API
  version: 1.2.2
  summary: XRPL XRP-drops payment rail (x402-style headers)
  description: |
    Sluzen sits in front of an HTTP API. Callers pay native XRP drops on the XRP Ledger.
    The issuer sets the price and receive address. Sluzen submits a payment the agent
    already signed, waits for ledger success, then forwards (proxy) or confirms settle
    (facilitator). Sluzen is not a wallet, exchange, or merchant of record.

    Trust boundary: on the default proxy, Sluzen does not call the origin until the
    ledger returns tesSUCCESS. On a facilitator resource the issuer calls
    `/facilitator/settle` after the handler. If the issuer never settles, the caller
    is not charged. The caller then depends on the issuer to settle, and on Sluzen
    being available for that settle, unless they verify the payment on-chain. After
    tesSUCCESS there is no refund. Sluzen does not see the facilitator resource body.

    Wire headers (CORS `exposedHeaders` on the API): `PAYMENT-REQUIRED`, `PAYMENT-RESPONSE`,
    `SLUZEN-CALL-ID`. Callers also send `PAYMENT-SIGNATURE` on the paid retry.

    `PAYMENT-REQUIRED` is base64(JSON `X402Challenge`). It is a challenge quote to sign,
    not a cryptographic signature artifact.

    Rate limits: global and per-route buckets from `resolveRateLimitConfig`
    (`RATE_LIMIT_*` env, defaults in `src/lib/limits.ts`). Exact maxima are deployment-configured;
    do not hardcode client-side. Live/proof share `liveMax`; discovery uses `discoveryMax`.

    Human wire guide (proxy decode → sign → paid retry): `docs/HTTP_402_GUIDE.md`
    and https://sluzen.network/guides/http-402. Synthetic challenge fixtures:
    `docs/fixtures/`.

    Admin and onboard surfaces are out of this public contract. See
    `docs/openapi-admin-stub.yaml` and operator docs.
  contact:
    name: Sluzen
    url: https://sluzen.network
  license:
    name: ISC

servers:
  - url: https://api.sluzen.network
    description: Production API
  - url: http://127.0.0.1:3000
    description: Local Fastify (`npm run dev`)

tags:
  - name: Proxy
    description: |
      Product path: any active `ProtectedEndpoint` with `surface=proxy` is served by the
      Fastify notFoundHandler at that `sourceRoute` (example below: `/api/v1/investor-demo`).

      Workflow (caller / agent):
      1. Request the registered route without `PAYMENT-SIGNATURE` → HTTP 402, body
         `{ error, message, callId? }`, headers `PAYMENT-REQUIRED` + `SLUZEN-CALL-ID`.
      2. Decode `PAYMENT-REQUIRED` (base64 JSON `X402Challenge`).
      3. Sign an XRPL Payment for `amount` drops to `destination`, memo type
         `sluzen/challenge-nonce` with the challenge `nonce`.
      4. Retry the same URL with header `PAYMENT-SIGNATURE` = signed tx blob.
      5. On success: upstream body (variable content-type), `PAYMENT-RESPONSE` = tx hash,
         `SLUZEN-CALL-ID` = same attempt id.

      Prefer `@sluzen/sdk` `AgentClient` for this negotiation. Settlement fail returns 402
      without `PAYMENT-REQUIRED` (or 409 replay); do not treat every 402 as a new challenge.

      Narrative + fixture: `docs/HTTP_402_GUIDE.md` · https://sluzen.network/guides/http-402

      Modes (endpoint flag `settleAfterOrigin`, description only — no extra paths):
      - Default: confirm Payment on ledger, then forward to origin. The buyer does not
        depend on the issuer server to settle.
      - `settleAfterOrigin=true`: origin must return 2xx first, then Sluzen submits the
        Payment. Requires rail terms acceptance for that issuer key.
  - name: Facilitator
    description: |
      Issuer-side helpers. The resource is an https URL registered with `surface=facilitator`
      (exact URL, no query/hash). Callers hit the issuer origin; the issuer server calls Sluzen.

      Workflow:
      1. `POST /facilitator/challenge` → 402 + `PAYMENT-REQUIRED` (+ `callId`).
      2. Caller pays (signs XRPL Payment) using the challenge.
      3. Optional `POST /facilitator/verify` → `{ isValid, callId?, cause? }` (always HTTP 200
         when the resource loads; invalid payments set `isValid: false`).
      4. Issuer runs its handler, then `POST /facilitator/settle` with the same signature.
      5. Success: `{ success, txHash, callId, network }`.

      Prefer `@sluzen/sdk` `facilitatorChallenge` / `facilitatorVerify` / `facilitatorSettle`
      so settle runs only after a successful handler.
  - name: Telemetry
    description: Public health, live feed, settlement proof, and announced-route discovery.

paths:
  /api/v1/investor-demo:
    get:
      tags: [Proxy]
      operationId: proxyInvestorDemoGet
      summary: Showcase proxy route (same contract as any active proxy sourceRoute)
      description: |
        Documented example of a registered proxy `sourceRoute`. Any other active proxy
        endpoint behaves identically under its own path. Without `PAYMENT-SIGNATURE`,
        issues a challenge. With a valid signed blob, settles then returns the upstream body
        (default mode) or settles after a 2xx origin (`settleAfterOrigin`).

        Other HTTP methods on a registered proxy path are also forwarded to the origin
        when payment succeeds; this operation documents the GET challenge/settle shape.
      parameters:
        - $ref: "#/components/parameters/PaymentSignatureHeader"
      responses:
        "200":
          description: |
            Paid call succeeded. Body is the upstream response (content-type variable).
            Headers include `PAYMENT-RESPONSE` (tx hash) and `SLUZEN-CALL-ID` when known.
          headers:
            PAYMENT-RESPONSE:
              $ref: "#/components/headers/PaymentResponse"
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                description: Typical JSON upstream; other content-types are possible.
                type: object
                additionalProperties: true
            "*/*":
              schema:
                type: string
                format: binary
        "402":
          description: |
            Unpaid challenge (includes `PAYMENT-REQUIRED`) OR settlement/bind failure
            (no `PAYMENT-REQUIRED`). Distinguish by presence of the header.
          headers:
            PAYMENT-REQUIRED:
              $ref: "#/components/headers/PaymentRequired"
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "403":
          description: Missing classic receive address, or terms not accepted for the issuer key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              examples:
                missingReceive:
                  value:
                    error: Receive Wallet Required
                    message: This route is not for sale until the issuer sets a classic XRPL receive address (r…).
                    cause: missing_merchant_receive_address
                termsRequired:
                  value:
                    error: Terms Required
                    message: Accept the current Terms and Privacy before registering a route or selling on the rail.
                    cause: terms_required
        "404":
          description: Path is not an active proxy endpoint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Not Found
                message: Route not found
        "409":
          description: >
            Payment or nonce replay (`tx_replay` / `nonce_replay`). Body may include
            `txHash` when a prior settlement_ok exists. Within the same proxy process
            and TTL, a paid retry with the same PAYMENT-SIGNATURE may instead return
            the cached upstream body (2xx) without a second ledger submit.
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Payment Replay
                message: Payment settlement failed: transaction already used
                cause: tx_replay
                callId: 550e8400-e29b-41d4-a716-446655440000
                txHash: "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"
        "429":
          description: Issuer prepaid credits cannot cover platform take (`quota_exceeded`).
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Quota Exceeded
                message: Issuer prepaid credits are insufficient for this call
                cause: quota_exceeded
        "502":
          description: Upstream unsafe/failed, or invalid upstream JSON when JSON was expected.
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "504":
          description: Upstream request timed out (`Upstream Timeout`).
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Upstream Timeout
                message: Upstream request timed out

  /facilitator/supported:
    get:
      tags: [Facilitator]
      operationId: facilitatorSupported
      summary: Supported payment kinds
      responses:
        "200":
          description: Exact XRP on XRPL only.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FacilitatorSupported"
              example:
                kinds:
                  - scheme: exact
                    network: xrpl:1
                    asset: XRP

  /facilitator/challenge:
    post:
      tags: [Facilitator]
      operationId: facilitatorChallenge
      summary: Issue a payment challenge for a registered facilitator resource
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FacilitatorResourceBody"
      responses:
        "402":
          description: Challenge issued. Decode `PAYMENT-REQUIRED` as `X402Challenge`.
          headers:
            PAYMENT-REQUIRED:
              $ref: "#/components/headers/PaymentRequired"
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentRequiredBody"
        "400":
          description: `resource` is not a valid https URL without query/hash.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "403":
          description: Rail terms missing, or no classic receive address.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: Resource not registered as an active facilitator endpoint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Not Found
                message: Resource not registered
        "429":
          description: Issuer credits insufficient before challenge (`quota_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"

  /facilitator/verify:
    post:
      tags: [Facilitator]
      operationId: facilitatorVerify
      summary: Validate a signed payment without consuming the challenge or submitting
      description: |
        Returns HTTP 200 even when the payment is invalid (`isValid: false`).
        Does not submit to the ledger and does not consume the challenge nonce.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FacilitatorPaymentBody"
      responses:
        "200":
          description: Verification result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FacilitatorVerifyResult"
        "400":
          description: Invalid resource URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "403":
          description: Terms or receive-address gate failed while loading the resource.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: Resource not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"

  /facilitator/settle:
    post:
      tags: [Facilitator]
      operationId: facilitatorSettle
      summary: Submit a verified signed payment for a facilitator resource
      description: |
        Consumes the challenge, reserves issuer take when applicable, submits the signed
        Payment, and returns the ledger tx hash. Optional `originAssertion` is logged
        blind on `settlement_ok` only (requires TERMS_RAIL 2026-10-14). Issuer handler
        side-effects belong in the SDK wrapper, not in this request body.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FacilitatorSettleBody"
      responses:
        "200":
          description: Ledger settlement succeeded.
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FacilitatorSettleSuccess"
        "400":
          description: Invalid resource URL or invalid `originAssertion`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "402":
          description: Settlement/bind failed (no new `PAYMENT-REQUIRED` challenge header).
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "403":
          description: Rail terms missing (`rail_terms_required`) or receive-address gate.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "404":
          description: Resource not registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "409":
          description: >
            Replay (`tx_replay` / `nonce_replay`). Includes `txHash` when a prior
            settlement_ok is known. Does not return the issuer handler body (that
            stays on the issuer process; see SDK `facilitatorSettle` in-process cache).
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Payment Replay
                message: Payment settlement failed: transaction already used
                cause: tx_replay
                callId: 550e8400-e29b-41d4-a716-446655440000
                txHash: "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"
        "429":
          description: Issuer prepaid credits insufficient (`quota_exceeded`).
          headers:
            SLUZEN-CALL-ID:
              $ref: "#/components/headers/SluzenCallId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"

  /health:
    get:
      tags: [Telemetry]
      operationId: getHealth
      summary: Liveness and XRPL rail probe
      responses:
        "200":
          description: Process up; `rail` reflects cached ledger probe (`open` | `closed`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
              example:
                status: ok
                rail: open
                network: xrpl:1
                environment: mainnet

  /api/v1/fx/xrp-usd:
    get:
      tags: [Telemetry]
      operationId: getXrpUsdFx
      summary: Public XRP/USD quote for console display
      description: |
        CoinGecko then Kraken. Cached ~20s on this route only.
        Does not change the 402 Amount (still XRP drops). Always HTTP 200.
        `available: false` when both sources miss. Not a fee schedule.
      responses:
        "200":
          description: Quote or unavailable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/XrpUsdFx"
              examples:
                available:
                  value:
                    available: true
                    usdPerXrp: "2.00000000"
                    source: coingecko
                    quotedAt: "2026-10-27T16:00:00.000Z"
                unavailable:
                  value:
                    available: false

  /api/v1/live:
    get:
      tags: [Telemetry]
      operationId: getLive
      summary: Settlement telemetry feed
      description: |
        Public response redacts `sourceRoute` (keeps at most two path segments, then `/***`)
        and truncates `txHash` (first 8 hex chars + ellipsis). Omits `callId` and several
        admin-only fields. With Bearer `ADMIN_TOKEN` or an API key scope `live:read`,
        `detail` becomes `full` (up to 50 recent events, unredacted fields including `callId`).
        Rate limit bucket: `liveMax` / `timeWindow`.
      security:
        - {}
        - bearerAuth: []
      responses:
        "200":
          description: Aggregates plus recent events.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiveStats"

  /api/v1/proof:
    get:
      tags: [Telemetry]
      operationId: getProof
      summary: Allowlisted showcase settlement
      description: |
        Latest `settlement_ok` on allowlisted `sourceRoute` values (default
        `/api/v1/investor-demo`; override via `PROOF_SOURCE_ROUTES`). Optional
        `PROOF_CALL_ID` pins a specific call. Full `txHash` and `callId` when present.
        Same rate-limit bucket as live (`liveMax`).
      responses:
        "200":
          description: Showcase proof available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Proof"
        "404":
          description: No allowlisted settlement proof.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Not Found
                message: No allowlisted settlement proof available

  /api/v1/discovery:
    get:
      tags: [Telemetry]
      operationId: getDiscovery
      summary: Announced sellable proxy routes
      description: |
        Public, unauthenticated. Lists active proxy routes with a non-withdrawn announcement,
        product price at or above the floor, issuer key bound, current terms acceptance on file,
        receive address set for that currency, and (on mainnet Split) enough prepaid credits
        (or an active Introductory Fee Waiver) to cover the 0.8% take. A row also requires
        proof: a `settlement_ok` for that route and issuer key, or a fresh HTTP 402 probe
        (TTL 15 minutes) against the public proxy URL. Does not expose `targetUrl`, receive
        address, issuer file fields, or buyer volume. Rate limit bucket: `discoveryMax` /
        `timeWindow`.
      responses:
        "200":
          description: Snapshot of discoverable routes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Discovery"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Optional for `GET /api/v1/live` full detail. Accepts operator `ADMIN_TOKEN` or an
        API key with scope `live:read`. Not used for proxy payment. Never put real tokens
        in public examples.

  parameters:
    PaymentSignatureHeader:
      name: PAYMENT-SIGNATURE
      in: header
      required: false
      description: Signed XRPL Payment transaction blob (hex). Required for the paid retry.
      schema:
        type: string
        minLength: 1

  headers:
    PaymentRequired:
      description: |
        Base64-encoded JSON `X402Challenge`. Present on unpaid challenge 402 responses only.
        Not a cryptographic signature.
      schema:
        type: string
    PaymentResponse:
      description: Settled XRPL transaction hash (hex).
      schema:
        type: string
        pattern: "^[A-Fa-f0-9]{64}$"
    SluzenCallId:
      description: Durable attempt id (`PaymentChallenge.id`). Same across 402, settle, and delivery.
      schema:
        type: string

  schemas:
    ErrorBody:
      type: object
      required: [error, message]
      additionalProperties: false
      properties:
        error:
          type: string
        message:
          type: string
        cause:
          type: string
        callId:
          type: string
        txHash:
          type: string
          description: >
            Present on payment replay (`tx_replay` / `nonce_replay`) when a prior
            `settlement_ok` is known. Confirms settle already happened; does not
            redeliver the resource body.

    PaymentRequiredBody:
      type: object
      required: [error, message]
      additionalProperties: false
      properties:
        error:
          type: string
          example: Payment Required
        message:
          type: string
          example: Payment is required to access this resource
        callId:
          type: string
          description: Same value as `SLUZEN-CALL-ID` when issued.

    X402Accept:
      type: object
      required: [scheme, network, asset, payTo, maxAmountRequired, extra]
      additionalProperties: false
      properties:
        scheme:
          type: string
          const: exact
        network:
          type: string
          enum: [xrpl:0, xrpl:1, XRPL]
          description: CAIP-2 XRPL network id. Legacy XRPL only when environment is unknown.
        asset:
          type: string
          const: XRP
        payTo:
          type: string
          description: Classic XRPL address (issuer receive).
        maxAmountRequired:
          type: string
          description: Integer XRP drops as a decimal string.
        extra:
          type: object
          required: [nonce, memoType]
          additionalProperties: false
          properties:
            nonce:
              type: string
            callId:
              type: string
            memoType:
              type: string
              const: sluzen/challenge-nonce

    X402Challenge:
      type: object
      required: [network, currency, amount, destination, expiration, nonce]
      additionalProperties: false
      description: |
        Decoded body of `PAYMENT-REQUIRED` (base64 JSON). `amount` is integer XRP drops,
        not USD microcents. Optional `accepts[]` is a generic x402 view of the same quote.
      properties:
        network:
          type: string
          example: xrpl:1
        currency:
          type: string
          example: XRP
        amount:
          type: integer
          minimum: 1
          description: Integer XRP drops.
        destination:
          type: string
          description: Classic XRPL receive address for this challenge.
        expiration:
          type: integer
          description: Unix time (seconds) when the challenge expires.
        nonce:
          type: string
          description: Must appear in Payment Memos (`sluzen/challenge-nonce`).
        accepts:
          type: array
          items:
            $ref: "#/components/schemas/X402Accept"
        callId:
          type: string
          description: Durable attempt id; may be absent on very old challenges.

    FacilitatorResourceBody:
      type: object
      required: [resource]
      additionalProperties: false
      properties:
        resource:
          type: string
          minLength: 1
          maxLength: 2048
          description: Exact https URL registered with `surface=facilitator` (no query/hash).
          example: https://issuer.example/v1/tide

    FacilitatorPaymentBody:
      type: object
      required: [resource, paymentSignature]
      additionalProperties: false
      properties:
        resource:
          type: string
          minLength: 1
          maxLength: 2048
        paymentSignature:
          type: string
          minLength: 1
          maxLength: 20000
          description: Signed XRPL Payment tx blob.

    FacilitatorSettleBody:
      type: object
      required: [resource, paymentSignature]
      additionalProperties: false
      properties:
        resource:
          type: string
          minLength: 1
          maxLength: 2048
        paymentSignature:
          type: string
          minLength: 1
          maxLength: 20000
          description: Signed XRPL Payment tx blob.
        originAssertion:
          type: string
          maxLength: 256
          description: |
            Optional blind Origin Assertion (alphanumeric digest or external receipt
            reference). Logged on settlement_ok only. Operator does not verify content.
            Requires TERMS_RAIL_VERSION 2026-10-14 clickwrap.

    FacilitatorSupported:
      type: object
      required: [kinds]
      additionalProperties: false
      properties:
        kinds:
          type: array
          items:
            type: object
            required: [scheme, network, asset]
            additionalProperties: false
            properties:
              scheme:
                type: string
                const: exact
              network:
                type: string
                enum: [xrpl:0, xrpl:1, XRPL]
                description: CAIP-2 XRPL network id. Legacy XRPL only when environment is unknown.
              asset:
                type: string
                const: XRP

    FacilitatorVerifyResult:
      type: object
      required: [isValid]
      additionalProperties: false
      properties:
        isValid:
          type: boolean
        callId:
          type: string
        cause:
          type: string
          description: Present when `isValid` is false (e.g. bind error or `quota_exceeded`).

    FacilitatorSettleSuccess:
      type: object
      required: [success, txHash, network]
      additionalProperties: false
      properties:
        success:
          type: boolean
          const: true
        txHash:
          type: string
        callId:
          type: string
        network:
          type: string
          enum: [xrpl:0, xrpl:1, XRPL]
          description: CAIP-2 XRPL network id. Legacy XRPL only when environment is unknown.

    Health:
      type: object
      required: [status, rail, network, environment]
      additionalProperties: false
      properties:
        status:
          type: string
          const: ok
        rail:
          type: string
          enum: [open, closed]
        network:
          type: string
          enum: [xrpl:0, xrpl:1, XRPL]
          description: CAIP-2 XRPL network id. Legacy XRPL only when environment is unknown.
        environment:
          type: string
          enum: [testnet, mainnet, unknown]

    XrpUsdFx:
      oneOf:
        - type: object
          required: [available, usdPerXrp, source, quotedAt]
          additionalProperties: false
          properties:
            available:
              type: boolean
              const: true
            usdPerXrp:
              type: string
              description: USD per 1 XRP (decimal string).
            source:
              type: string
              description: Quote provider id (e.g. coingecko, kraken).
            quotedAt:
              type: string
              format: date-time
        - type: object
          required: [available]
          additionalProperties: false
          properties:
            available:
              type: boolean
              const: false

    LiveTotals:
      type: object
      required:
        - activeEndpoints
        - challengesIssued
        - settlementsOk
        - settlementsFailed
      additionalProperties: false
      properties:
        activeEndpoints:
          type: integer
        challengesIssued:
          type: integer
        settlementsOk:
          type: integer
        settlementsFailed:
          type: integer

    LiveEvent:
      type: object
      required: [id, type, sourceRoute, createdAt]
      additionalProperties: false
      description: |
        Public events omit `callId`, `httpStatus`, `message`, and take/FX fields.
        `failCause` is present on `settlement_fail` for public and full detail.
        Full detail includes free-text `message` when authorized.
      properties:
        id:
          type: string
        type:
          type: string
          description: e.g. challenge, settlement_ok, settlement_fail, delivery
        callId:
          type: string
          description: Full detail only.
        sourceRoute:
          type: string
          description: Redacted in public detail.
        currency:
          type: string
        amountDrops:
          type: integer
        txHash:
          type: string
          description: Truncated in public detail; full hex when `detail=full`.
        httpStatus:
          type: integer
          description: Full detail only.
        latencyMs:
          type: integer
        message:
          type: string
          description: Full detail only. Stripped of the internal failCause prefix.
        failCause:
          type: string
          enum:
            - quota_short
            - price_mismatch
            - dest_mismatch
            - payment_replay
            - challenge_invalid
            - ledger_reject
            - payment_invalid
            - unknown
          description: |
            Public-safe cause bucket for settlement_fail. Omitted on other types.
        createdAt:
          type: string
          format: date-time
        takeDrops:
          type: integer
          description: Full detail only.
        fxUsdPerXrp:
          type: string
          description: Full detail only.
        takeUsd:
          type: string
          description: Full detail only.
        fxSource:
          type: string
          description: Full detail only.

    LiveStats:
      type: object
      required: [generatedAt, network, environment, detail, totals, recent]
      additionalProperties: false
      properties:
        generatedAt:
          type: string
          format: date-time
        network:
          type: string
          enum: [xrpl:0, xrpl:1, XRPL]
          description: CAIP-2 XRPL network id. Legacy XRPL only when environment is unknown.
        environment:
          type: string
          enum: [testnet, mainnet, unknown]
        detail:
          type: string
          enum: [public, full]
        totals:
          $ref: "#/components/schemas/LiveTotals"
        recent:
          type: array
          items:
            $ref: "#/components/schemas/LiveEvent"

    Proof:
      type: object
      required: [callId, txHash, sourceRoute, createdAt, network, environment]
      additionalProperties: false
      properties:
        callId:
          type: string
        txHash:
          type: string
        amountDrops:
          type: integer
        currency:
          type: string
        sourceRoute:
          type: string
        createdAt:
          type: string
          format: date-time
        network:
          type: string
          enum: [xrpl:0, xrpl:1, XRPL]
          description: CAIP-2 XRPL network id. Legacy XRPL only when environment is unknown.
        environment:
          type: string
          enum: [testnet, mainnet, unknown]
        httpStatus:
          type: integer
          description: From matching `delivery` event when present.
        latencyMs:
          type: integer
          description: From matching `delivery` event when present.

    DiscoveryRoute:
      type: object
      required: [url, sourceRoute, priceDrops, currency, network, label]
      additionalProperties: false
      properties:
        url:
          type: string
          format: uri
        sourceRoute:
          type: string
        priceDrops:
          type: integer
          description: |
            XRP drops when currency is XRP. Zero when currency is RLUSD or USDC
            (product price is iouValue or usdcAtomic).
        currency:
          type: string
          enum: [XRP, RLUSD, USDC]
          description: |
            Product currency on the announced row. Live discovery lists sellable
            announced proxy routes in XRP, allowlisted RLUSD, or canonical Base
            USDC when announce rules (floor, terms, take coverage) hold.
        network:
          type: string
          enum: [xrpl:0, xrpl:1, XRPL, eip155:8453, eip155:84532]
          description: |
            CAIP-2 network id. XRPL for XRP/RLUSD (legacy XRPL only when
            environment is unknown). Base CAIP for USDC.
        iouValue:
          type: string
          description: RLUSD product Amount value when currency is RLUSD.
        usdcAtomic:
          type: integer
          description: USDC atomic units (6 decimals) when currency is USDC.
        asset:
          type: string
          description: Canonical Circle USDC contract when currency is USDC.
        label:
          type: string
        category:
          type: string
          enum: [market-data, other]
          description: |
            Optional vertical tag set by the issuer. Omitted when unset.
            Empty discovery lists are valid; no fabricated third-party rows.

    Discovery:
      type: object
      required: [generatedAt, network, environment, routes]
      additionalProperties: false
      properties:
        generatedAt:
          type: string
          format: date-time
        network:
          type: string
          enum: [xrpl:0, xrpl:1, XRPL]
          description: CAIP-2 XRPL network id. Legacy XRPL only when environment is unknown.
        environment:
          type: string
          enum: [testnet, mainnet, unknown]
        routes:
          type: array
          items:
            $ref: "#/components/schemas/DiscoveryRoute"
