openapi: 3.1.0
info:
  title: JIL Sovereign Pre-Clearance API
  version: '2026.05.03'
  summary: Non-custodial cryptographic pre-clearance attestation for institutional crypto transfers.
  description: |
    The JIL Pre-Clearance Layer issues Pre-Settlement Attestations (PSAs) for
    institutional crypto transfers between custodians and exchanges. Custodians
    `POST /v1/preclearance/validate` before signing. The asset never enters JIL;
    we attest, the custodian decides.

    A PSA contains:
      - 4-stage pipeline findings (Endpoints, KYC/AML, Sanctions, Risk)
      - Overall verdict (`pass` / `flag` / `fail`)
      - Validity window (default 60s, configurable)
      - Signatures from the 14-of-20 BFT validator quorum
      - Fee record (25 bps default, $1 floor)

    Receivers (exchanges) can call `POST /v1/preclearance/verify` with a PSA + signature
    to confirm authenticity without redoing the work.

    Authentication is HMAC-SHA256 over the canonical request bytes. See the
    `/security/hmac-signing` schema below.
  contact:
    name: JIL Sovereign Engineering
    url: https://jilsovereign.com/pillars/pre-clearance
    email: engineering@jilsovereign.com
  license:
    name: Proprietary
    url: https://jilsovereign.com/legal/api-terms

servers:
  - url: https://preclearance.jilsovereign.com
    description: Production
  - url: https://testnet.jilsovereign.com/api/preclearance
    description: Pilot Sandbox (basic auth)

security:
  - hmac: []
  - bearer: []

tags:
  - name: Validate
    description: Custodian-side endpoints — call before signing a transfer.
  - name: Verify
    description: Exchange-side endpoints — confirm PSA authenticity.
  - name: Read
    description: Retrieve previously-issued PSAs.
  - name: Ops
    description: Health, readiness, metrics, public-key.

paths:
  /v1/preclearance/validate:
    post:
      tags: [Validate]
      summary: Issue a Pre-Settlement Attestation for a pending transfer.
      description: |
        Run the 4-stage pre-clearance pipeline against a pending transit and
        return a signed PSA. Custodian-side endpoint — typically called from a
        Transaction Authorization Policy (TAP) hook.

        Latency: p50 300-500ms, p99 1.5s. Validity window: 60s (custodian
        must complete the on-chain transfer within that window or re-validate).

        Idempotency: a `transit_id` that has already been issued a PSA in the
        last 60s returns the cached PSA (no re-billing).
      operationId: validateTransit
      parameters:
        - in: query
          name: finality
          schema:
            type: string
            enum: [final, optimistic]
            default: final
          description: |
            `final` — wait for full 14-of-20 quorum signing (canonical).
            `optimistic` — return on operator signature with quorum committed
            asynchronously. Use only for high-throughput, low-value transits.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
            examples:
              fireblocks_eth_transfer:
                summary: Custodian -> Exchange ETH transfer
                value:
                  transit_id: fb-2026050301-7c3a
                  source_addr: '0x7Bcff27567cfE3e67020a0d771a445178756aBa2'
                  destination_addr: '0xbeefdeadbeefdeadbeefdeadbeefdeadbeefdead'
                  asset: ETH
                  amount_native: '12.5'
                  amount_usd: 41250.00
                  sender_id: fund-tax-id-37-3892034
                  receiver_id: kraken-acct-2391
                  custodian: fireblocks
                  exchange: kraken
                  client_metadata:
                    desk: trading
                    initiator: ops_user_47
      responses:
        '200':
          description: PSA issued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403':
          description: Custodian mismatch — the authenticated customer slug does not match `custodian` in the body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429': { $ref: '#/components/responses/RateLimited' }
        '503':
          description: Quorum unavailable — fewer than 14 validators signed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/preclearance/verify:
    post:
      tags: [Verify]
      summary: Verify a PSA's quorum signature.
      description: |
        Exchange-side endpoint. Given a PSA payload + signature, confirms the
        operator validator's Ed25519 signature against the canonical bytes.
        Production extends to iterate all 14-of-20 signatures.
      operationId: verifyPsa
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRequest'
      responses:
        '200':
          description: Verification result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/preclearance/psa/{id}:
    get:
      tags: [Read]
      summary: Retrieve a previously-issued PSA.
      description: |
        Returns the full PSA + signatures. Privacy: only the custodian or
        exchange named in the PSA may read it (or a service-account caller).
      operationId: getPsa
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: PSA found.
          content:
            application/json:
              schema:
                type: object
                properties:
                  psa: { $ref: '#/components/schemas/PSA' }
                  signatures:
                    type: array
                    items: { $ref: '#/components/schemas/Signature' }
        '403':
          description: Caller is not a party to this PSA.
        '404':
          description: PSA not found.
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/preclearance/stats:
    get:
      tags: [Read]
      summary: Aggregate stats over the last 24 hours.
      operationId: stats24h
      responses:
        '200':
          description: Stats.
          content:
            application/json:
              schema:
                type: object
                properties:
                  window: { type: string, example: '24h' }
                  total: { type: integer }
                  pass: { type: integer }
                  flag: { type: integer }
                  fail: { type: integer }
                  total_value_usd: { type: number }
                  total_fee_usd: { type: number }

  /v1/preclearance/public-key:
    get:
      tags: [Ops]
      summary: Operator validator's Ed25519 public key (for `verify`).
      operationId: getPublicKey
      security: []
      responses:
        '200':
          description: Public key + quorum metadata.
          content:
            application/json:
              schema:
                type: object
                properties:
                  operator:
                    type: object
                    properties:
                      alg: { type: string, example: ed25519 }
                      public_key_pem: { type: string }
                  quorum:
                    type: object
                    properties:
                      required: { type: integer, example: 14 }
                      total: { type: integer, example: 20 }
                      jurisdictions: { type: integer, example: 13 }

  /health:
    get:
      tags: [Ops]
      summary: Service health (DB + breakers + rate-limit + travel-rule status).
      operationId: health
      security: []
      responses:
        '200': { description: healthy }
        '503': { description: degraded (DB unreachable) }

  /ready:
    get:
      tags: [Ops]
      summary: Readiness probe (DB only).
      operationId: ready
      security: []
      responses:
        '200': { description: ready }
        '503': { description: not ready }

  /metrics:
    get:
      tags: [Ops]
      summary: Prometheus metrics.
      operationId: metrics
      security: []
      responses:
        '200':
          description: text/plain Prometheus exposition.
          content:
            text/plain:
              schema: { type: string }

components:
  securitySchemes:
    hmac:
      type: apiKey
      in: header
      name: X-JIL-Signature
      description: |
        HMAC-SHA256 over canonical request bytes. Required headers:
          - `X-JIL-Access-Key-Id`  — your access key ID
          - `X-JIL-Timestamp`      — ISO-8601 (request rejected if > 60s skew)
          - `X-JIL-Nonce`          — random hex (replay protection, 5-min window)
          - `X-JIL-Signature`      — base64(hmac_sha256(secret, canonical))

        Canonical = `{method}\n{path}\n{timestamp}\n{nonce}\n{sha256(body)}`.
    bearer:
      type: http
      scheme: bearer
      description: |
        Bearer token fallback for environments without HMAC libraries. NOT
        recommended for production: no replay protection. Use HMAC.

  responses:
    BadRequest:
      description: Validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing or invalid auth headers.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: Per-customer rate limit exceeded (default 100 req/s sustained, 200 burst).
      headers:
        Retry-After:
          schema: { type: integer }
        X-RateLimit-Limit:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    ValidateRequest:
      type: object
      description: |
        Phase 1 (today): the optional `transaction_preimage_hash`,
        `chain_id`, `block_height_window_*`, `client_id`, `egress_request_uuid`,
        and `route_attestation` fields enable pre-image binding (Design Doc §6.4)
        and full domain separation (§5.4). Custodians SHOULD provide all of them
        for production egress; without them, the PSA reverts to advisory mode.
      required:
        - transit_id
        - source_addr
        - destination_addr
        - asset
        - amount_native
        - sender_id
        - receiver_id
        - custodian
        - exchange
      properties:
        transit_id:
          type: string
          minLength: 8
          maxLength: 128
          description: Caller-generated unique ID for this transit (used for idempotency).
        source_addr:
          type: string
          description: Custodian-side address (where funds leave).
        destination_addr:
          type: string
          description: Exchange-side address (where funds land).
        asset:
          type: string
          example: ETH
          description: Asset symbol (BTC, ETH, USDC, SOL, etc.).
        amount_native:
          type: string
          pattern: '^\d+(\.\d{1,18})?$'
          description: Decimal string (avoid float precision). e.g. "12.5" for 12.5 ETH.
        amount_usd:
          type: number
          minimum: 0
          description: Optional USD-denominated value (used for fee + risk band).
        sender_id:
          type: string
          description: Custodian-internal sender ID (account, fund, KYB entity).
        receiver_id:
          type: string
          description: Exchange-internal receiver ID.
        custodian:
          type: string
          example: fireblocks
        exchange:
          type: string
          example: kraken
        client_metadata:
          type: object
          additionalProperties: true
        transaction_preimage_hash:
          type: string
          pattern: '^(0x)?[0-9a-fA-F]{64}$'
          description: |
            Phase 1 (Design Doc §6.4): sha256 of the canonical raw transaction
            bytes the custodian intends to broadcast. Bound into the signature
            domain so any drift between attested and broadcast invalidates the
            signature. Optional today, REQUIRED for Phase 2 cosigning.
        chain_id:
          oneOf: [{ type: string }, { type: integer }]
          description: EIP-155 chain ID for EVM, network identifier otherwise (e.g. 'mainnet', 'solana-mainnet').
        block_height_window_open:
          type: integer
          minimum: 0
          description: Earliest block height at which broadcast is valid.
        block_height_window_close:
          type: integer
          minimum: 0
          description: Latest block height at which broadcast is valid; window is canon-bound into the signature domain.
        client_id:
          type: string
          description: Client account identifier under the custodian (separates clients within the same custodian).
        egress_request_uuid:
          type: string
          format: uuid
          description: Single-use UUID for this egress request; replay-proof against re-use.
        route_attestation:
          type: object
          properties:
            venue_type: { type: string, enum: [cex, dex, bridge, direct] }
            venue_id: { type: string }
            intermediate_hops:
              type: array
              items: { type: string }

    ValidateResponse:
      type: object
      properties:
        psa: { $ref: '#/components/schemas/PSA' }
        signatures:
          type: array
          items: { $ref: '#/components/schemas/Signature' }
        bls_aggregate_b64:
          type: string
          nullable: true
        quorum:
          type: object
          properties:
            required: { type: integer, example: 14 }
            total: { type: integer, example: 20 }
            obtained: { type: integer }
            mode: { type: string, enum: [final, optimistic] }
        attribution:
          type: object
          properties:
            customer_id: { type: string, format: uuid }
            customer_slug: { type: string }
            auth_method: { type: string, enum: [hmac, bearer] }
            key_id: { type: string }

    VerifyRequest:
      type: object
      required: [psa, signature_b64]
      properties:
        psa:
          $ref: '#/components/schemas/PSA'
        signature_b64:
          type: string

    VerifyResponse:
      type: object
      properties:
        ok: { type: boolean }
        operator_verified: { type: boolean }
        quorum_verification: { type: string, example: 'full-14-of-20' }

    PSA:
      type: object
      properties:
        psa_id: { type: string, format: uuid }
        transit_id: { type: string }
        source_addr: { type: string }
        destination_addr: { type: string }
        asset: { type: string }
        amount_native: { type: string }
        amount_usd: { type: number }
        sender_id: { type: string }
        receiver_id: { type: string }
        custodian: { type: string }
        exchange: { type: string }
        issued_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        validity_window_sec: { type: integer, example: 60 }
        findings:
          type: array
          items: { $ref: '#/components/schemas/StageFinding' }
        overall_verdict: { type: string, enum: [pass, flag, fail] }
        fee:
          type: object
          properties:
            bps: { type: number, example: 25 }
            amount_usd: { type: number }
            floor_usd: { type: number, example: 1 }
        domain_separation:
          type: object
          description: Phase 1 (Design Doc §5.4) - signature domain tag preventing cross-chain / cross-window / cross-client replay.
          properties:
            chain_id: { type: string, nullable: true }
            jil_network_id: { type: string, example: jil-mainnet-1 }
            custodian: { type: string }
            client_id: { type: string, nullable: true }
            egress_request_uuid: { type: string, format: uuid }
            block_height_window_open: { type: integer, nullable: true }
            block_height_window_close: { type: integer, nullable: true }
            pocs_bundle_hash: { type: string, description: sha256 of the PoCS attestation bundle (findings array) }
            transaction_preimage_hash: { type: string, nullable: true, description: 'sha256 of the canonical raw txn bytes (Design Doc §6.4 pre-image binding)' }
        route_attestation:
          type: object
          nullable: true
          properties:
            venue_type: { type: string, enum: [cex, dex, bridge, direct] }
            venue_id: { type: string }
            intermediate_hops:
              type: array
              items: { type: string }

    StageFinding:
      type: object
      properties:
        stage: { type: string, enum: [endpoints, kyc, sanctions, risk] }
        status: { type: string, enum: [pass, flag, fail] }
        duration_ms: { type: integer }
        details:
          type: object
          additionalProperties: true

    Signature:
      type: object
      properties:
        validator_id: { type: string }
        jurisdiction: { type: string, example: US }
        alg: { type: string, example: ed25519 }
        signature_b64: { type: string }

    ErrorResponse:
      type: object
      properties:
        error: { type: string }
        detail: { type: string }
        issues:
          type: array
          items:
            type: object
            additionalProperties: true
