openapi: 3.1.0

info:
  title: Spout Partner API
  version: "2026-08-24"
  summary: Borrow against tokenized US equities at 0% interest, and lend stablecoins against covered-call premium.
  description: |
    The Spout Partner API lets an integrator surface Spout's two sides inside their own product:

    - **Borrowing.** A user holding tokenized US equities posts them as collateral and draws stablecoins at 0% interest.
    - **Lending.** A user deposits stablecoins into a two-tranche pool and earns yield funded by systematic
      covered calls written against borrower collateral.

    ## Status

    **This specification describes an API that is not yet live.** It exists so integration partners can design
    and build against a stable contract while the protocol is in testnet. Endpoint shapes are intended to be
    stable. Every number returned by the sandbox is fictional.

    ## The one thing to understand before reading further

    Spout is a non-custodial protocol on Solana. **This API never moves user funds and never holds a user's key.**

    Write endpoints do not execute anything. They return an unsigned, serialized Solana transaction that your
    application presents to the user's wallet for signature, and that you then submit to the network yourself.
    Read endpoints return protocol and position state.

    Concretely, opening a loan is three steps:

    1. `POST /v1/borrow/loans` returns `transaction.unsigned` (base64) and a `transaction.id`
    2. Your app has the user sign it and submits it to Solana
    3. You poll `GET /v1/transactions/{id}` or receive a webhook when it confirms

    If you were expecting a custodial API where `POST /loans` creates a loan, this is the difference that will
    shape your integration.

    ## Conventions

    **Authentication.** `Authorization: Bearer sk_test_...`. Keys are environment-scoped. Test keys only ever
    touch sandbox state.

    **Versioning.** The version is pinned per key and can be overridden per request with `Spout-Version`.
    Additive changes ship without a version bump. Breaking changes get a new dated version and the old one keeps working.

    **Idempotency.** Every `POST` accepts `Idempotency-Key`. Replaying a key returns the original response.
    Reusing a key with a different body is rejected with `idempotency_key_reuse`.

    **Pagination.** Cursor based, `starting_after` and `ending_before`, `limit` capped at 100. No offsets.

    **Amounts.** Decimal strings, never floats. `"1250.75"`. Paired with `asset` and `decimals` so you never
    have to guess the scale. Rationale in OPEN_QUESTIONS.md.

    **Request IDs.** Every response carries `Spout-Request-Id`. Quote it in support requests.

    **Errors.** Consistent envelope with a machine-readable `code`, a human `message`, and a `doc_url`.

  contact:
    name: Spout Partner Engineering
    url: https://spout.finance/docs
  license:
    name: Proprietary
    url: https://spout.finance/terms

servers:
  - url: https://sandbox.api.spout.finance
    description: Sandbox. Fictional data, no real funds, no signature required to read.
  - url: https://api.spout.finance
    description: Production. Not yet available.

security:
  - bearerAuth: []

tags:
  - name: Protocol
    description: Protocol-wide state and per-asset risk parameters.
  - name: Lending
    description: Stablecoin deposits into the Senior and Junior tranches.
  - name: Borrowing
    description: Collateralised 0% borrowing against tokenized equities.
  - name: Transactions
    description: Lifecycle of a transaction handed back for signature.
  - name: Webhooks
    description: Event delivery.

paths:

  /v1/protocol/stats:
    get:
      tags: [Protocol]
      summary: Protocol statistics
      description: Aggregate protocol state. Safe to cache for 60 seconds.
      operationId: getProtocolStats
      parameters:
        - $ref: "#/components/parameters/Version"
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProtocolStats" }
              example:
                object: protocol_stats
                tvl: { total: "52400000.00", senior: "44540000.00", junior: "7860000.00", currency: "USD" }
                collateral_value: "118600000.00"
                total_borrowed: "34060000.00"
                utilization: "0.65"
                reserve_ratio: "0.15"
                insurance_fund: { balance: "1310000.00", coverage_ratio: "0.025" }
                paused: false
                updated_at: "2026-08-24T14:30:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/assets:
    get:
      tags: [Protocol]
      summary: List supported collateral assets
      description: |
        Every asset accepted as collateral, with its risk parameters. An asset with `accepting_deposits: false`
        is still readable but will reject new collateral.
      operationId: listAssets
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/EndingBefore"
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                type: object
                required: [object, data, has_more]
                properties:
                  object: { type: string, const: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Asset" }
                  has_more: { type: boolean }
              example:
                object: list
                has_more: true
                data:
                  - object: asset
                    symbol: AAPL
                    name: Apple Inc.
                    mint: "AAPLmintPLACEHOLDER1111111111111111111111111"
                    asset_class: equity
                    max_ltv: "0.50"
                    liquidation_threshold: "0.60"
                    estimated_borrow_cost_apr: "0.0037"
                    cycle: weekly
                    price: { value: "232.50", currency: USD, as_of: "2026-08-24T20:00:00Z", stale: false }
                    accepting_deposits: true
                    decimals: 9
                  - object: asset
                    symbol: NVDA
                    name: NVIDIA Corporation
                    mint: "NVDAmintPLACEHOLDER1111111111111111111111111"
                    asset_class: equity
                    max_ltv: "0.50"
                    liquidation_threshold: "0.60"
                    estimated_borrow_cost_apr: "0.0000"
                    cycle: weekly
                    price: { value: "178.20", currency: USD, as_of: "2026-08-24T20:00:00Z", stale: false }
                    accepting_deposits: true
                    decimals: 9
                  - object: asset
                    symbol: GLD
                    name: SPDR Gold Shares
                    mint: "GLDmintPLACEHOLDER11111111111111111111111111"
                    asset_class: etf
                    max_ltv: "0.50"
                    liquidation_threshold: "0.60"
                    estimated_borrow_cost_apr: "0.0006"
                    cycle: biweekly
                    price: { value: "318.75", currency: USD, as_of: "2026-08-24T20:00:00Z", stale: false }
                    accepting_deposits: false
                    decimals: 9
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/assets/{symbol}:
    get:
      tags: [Protocol]
      summary: Retrieve one asset
      operationId: getAsset
      parameters:
        - $ref: "#/components/parameters/Version"
        - name: symbol
          in: path
          required: true
          schema: { type: string, examples: ["AAPL"] }
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Asset" }
              example:
                object: asset
                symbol: AAPL
                name: Apple Inc.
                mint: "AAPLmintPLACEHOLDER1111111111111111111111111"
                asset_class: equity
                max_ltv: "0.50"
                liquidation_threshold: "0.60"
                estimated_borrow_cost_apr: "0.0037"
                cycle: weekly
                price: { value: "232.50", currency: USD, as_of: "2026-08-24T20:00:00Z", stale: false }
                accepting_deposits: true
                decimals: 9
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/tranches:
    get:
      tags: [Lending]
      summary: List lending tranches
      description: |
        Senior and Junior. Returns current estimated APY, exit terms and position in the loss waterfall.

        APY figures are **estimates based on trailing realised cycles**, not guarantees. Surface them as
        estimates in your UI.
      operationId: listTranches
      parameters:
        - $ref: "#/components/parameters/Version"
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                type: object
                required: [object, data]
                properties:
                  object: { type: string, const: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Tranche" }
              example:
                object: list
                data:
                  - object: tranche
                    id: senior
                    display_name: Senior
                    estimated_apy: "0.0867"
                    loss_position: 2
                    exit: { instant_available: true, instant_haircut_range: ["0", "0.03"], queue_notice_days: 7 }
                    capacity: { deposited: "44540000.00", cap: "60000000.00" }
                    minimum_deposit: "100.00"
                  - object: tranche
                    id: junior
                    display_name: Junior
                    estimated_apy: "0.2840"
                    loss_position: 1
                    exit: { instant_available: false, instant_haircut_range: [], queue_notice_days: 45 }
                    capacity: { deposited: "7860000.00", cap: "10000000.00" }
                    minimum_deposit: "1000.00"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/lend/positions:
    get:
      tags: [Lending]
      summary: List lending positions for a wallet
      operationId: listLendPositions
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/Wallet"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/EndingBefore"
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                type: object
                required: [object, data, has_more]
                properties:
                  object: { type: string, const: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/LendPosition" }
                  has_more: { type: boolean }
              example:
                object: list
                has_more: false
                data:
                  - object: lend_position
                    id: lp_2Kf9wQ
                    wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                    tranche: senior
                    shares: "24810.442"
                    balance: "25640.18"
                    principal_deposited: "25000.00"
                    yield_earned_to_date: "640.18"
                    pending_withdrawal_id: null
                    created_at: "2026-05-02T09:14:00Z"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/lend/deposits:
    post:
      tags: [Lending]
      summary: Build a deposit transaction
      description: |
        Returns an unsigned transaction that deposits stablecoin into the chosen tranche. Nothing is deposited
        until your application has the user sign and submit it.
      operationId: createLendDeposit
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wallet, tranche, amount, asset]
              properties:
                wallet: { $ref: "#/components/schemas/WalletAddress" }
                tranche: { $ref: "#/components/schemas/TrancheId" }
                amount:
                  $ref: "#/components/schemas/Decimal"
                asset:
                  type: string
                  description: Stablecoin mint symbol.
                  examples: ["USDC"]
      responses:
        "200":
          description: Transaction built
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionEnvelope" }
              example:
                object: transaction_envelope
                transaction:
                  id: txn_6bW2nH
                  unsigned: "AQABA0pWZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
                  expires_at: "2026-08-24T14:32:30Z"
                simulation:
                  summary: "Deposit 25,000.00 USDC into the Senior tranche"
                  effects:
                    - { label: "USDC balance", before: "31,204.11", after: "6,204.11" }
                    - { label: "Senior position", before: "0.00", after: "25,000.00" }
                  network_fee: "0.000005"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/lend/withdrawals:
    post:
      tags: [Lending]
      summary: Build a withdrawal transaction
      description: |
        Exit behaviour differs by tranche and this is the most misunderstood part of the product.

        **Senior** offers an instant exit at a dynamic haircut based on reserve health, or a free queued exit
        at full NAV. Choose with `mode`.

        **Junior** has no instant exit. It always enters the queue with a notice period.

        Call this endpoint with `preview: true` first to show the user the haircut or the estimated wait
        before they commit.
      operationId: createLendWithdrawal
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wallet, tranche, amount]
              properties:
                wallet: { $ref: "#/components/schemas/WalletAddress" }
                tranche: { $ref: "#/components/schemas/TrancheId" }
                amount: { $ref: "#/components/schemas/Decimal" }
                mode:
                  type: string
                  enum: [instant, queue]
                  default: queue
                  description: Senior only. Junior ignores this and always queues.
                preview:
                  type: boolean
                  default: false
                  description: When true, returns terms without building a transaction.
      responses:
        "200":
          description: Preview or built transaction
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/WithdrawalPreview"
                  - $ref: "#/components/schemas/TransactionEnvelope"
              examples:
                preview:
                  summary: preview true, senior instant exit
                  value:
                    object: withdrawal_preview
                    mode: instant
                    amount: "10000.00"
                    haircut: "0.012"
                    receivable: "9880.00"
                    estimated_available_at: null
                preview_junior_queue:
                  summary: preview true, junior always queues
                  value:
                    object: withdrawal_preview
                    mode: queue
                    amount: "10000.00"
                    haircut: "0"
                    receivable: "10000.00"
                    estimated_available_at: "2026-10-08T00:00:00Z"
                built:
                  summary: preview false, transaction built for signature
                  value:
                    object: transaction_envelope
                    transaction:
                      id: txn_8dY4pM
                      unsigned: "AQABA0pWZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
                      expires_at: "2026-08-24T14:32:30Z"
                    simulation:
                      summary: "Withdraw 10,000.00 from Senior at a 1.2% haircut"
                      effects:
                        - { label: "Senior position", before: "25,640.18", after: "15,640.18" }
                        - { label: "USDC received", before: "0.00", after: "9,880.00" }
                      network_fee: "0.000005"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/lend/withdrawals/{id}:
    get:
      tags: [Lending]
      summary: Withdrawal queue status
      description: Position in queue, estimated fill, and amount filled so far.
      operationId: getLendWithdrawal
      parameters:
        - $ref: "#/components/parameters/Version"
        - name: id
          in: path
          required: true
          schema: { type: string, examples: ["wdr_3kQ1vN"] }
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WithdrawalRequest" }
              example:
                object: withdrawal_request
                id: wdr_3kQ1vN
                wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                tranche: junior
                status: partially_filled
                amount_requested: "50000.00"
                amount_filled: "18400.00"
                queue_position: 3
                estimated_available_at: "2026-10-08T00:00:00Z"
                created_at: "2026-08-24T11:02:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/lend/yield:
    get:
      tags: [Lending]
      summary: Realised yield history
      description: Per-cycle distributions credited to a wallet. Newest first.
      operationId: listLendYield
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/Wallet"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/EndingBefore"
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                type: object
                required: [object, data, has_more]
                properties:
                  object: { type: string, const: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/YieldDistribution" }
                  has_more: { type: boolean }
              example:
                object: list
                has_more: true
                data:
                  - object: yield_distribution
                    id: yd_7Ln4xT
                    wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                    tranche: senior
                    cycle_id: cyc_2026_34
                    amount: "41.62"
                    realized_apy: "0.0871"
                    distributed_at: "2026-08-22T21:15:00Z"
                  - object: yield_distribution
                    id: yd_7Ln4xS
                    wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                    tranche: senior
                    cycle_id: cyc_2026_33
                    amount: "39.08"
                    realized_apy: "0.0818"
                    distributed_at: "2026-08-15T21:15:00Z"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/borrow/power:
    get:
      tags: [Borrowing]
      summary: Borrowing power quote
      description: |
        How much a wallet could borrow against a specific holding right now.

        `max_borrowable` already accounts for the asset's max LTV, any collateral the wallet has already
        pledged, and the asset's current deposit status. It is a quote, not a reservation: it can move with
        the underlying price between the quote and the transaction landing.
      operationId: getBorrowPower
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/Wallet"
        - name: asset
          in: query
          required: true
          schema: { type: string, examples: ["AAPL"] }
        - name: collateral_amount
          in: query
          required: false
          description: Units of the asset to model. Defaults to the wallet's full balance.
          schema: { $ref: "#/components/schemas/Decimal" }
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BorrowPowerQuote" }
              example:
                object: borrow_power_quote
                wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                asset: AAPL
                collateral_amount: "45.4"
                collateral_value: "10555.50"
                max_ltv: "0.50"
                max_borrowable: "5277.75"
                already_pledged: "0"
                projected:
                  health_factor: "1.20"
                  liquidation_price: "193.75"
                quoted_at: "2026-08-24T14:30:00Z"
                expires_at: "2026-08-24T14:31:00Z"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/borrow/loans:
    get:
      tags: [Borrowing]
      summary: List loans for a wallet
      operationId: listLoans
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/Wallet"
        - name: status
          in: query
          schema:
            type: string
            enum: [active, at_risk, repaying, closed, liquidated]
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/EndingBefore"
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                type: object
                required: [object, data, has_more]
                properties:
                  object: { type: string, const: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Loan" }
                  has_more: { type: boolean }
              example:
                object: list
                has_more: false
                data:
                  - object: loan
                    id: loan_8fQz2R
                    wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                    status: active
                    asset: AAPL
                    collateral_amount: "45.4"
                    collateral_value: "10555.50"
                    borrow_asset: USDC
                    principal: "4222.20"
                    repaid_to_date: "0"
                    outstanding: "4222.20"
                    interest_accrued: "0"
                    current_ltv: "0.40"
                    health_factor: "1.50"
                    liquidation_price: "155.00"
                    available_to_borrow: "1055.55"
                    opened_at: "2026-07-11T15:42:00Z"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      tags: [Borrowing]
      summary: Build a loan transaction
      description: |
        Returns an unsigned transaction that pledges collateral and draws stablecoin in one instruction set.
        No loan exists until it is signed and confirmed.
      operationId: createLoan
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wallet, asset, collateral_amount, borrow_amount]
              properties:
                wallet: { $ref: "#/components/schemas/WalletAddress" }
                asset: { type: string, examples: ["AAPL"] }
                collateral_amount: { $ref: "#/components/schemas/Decimal" }
                borrow_amount: { $ref: "#/components/schemas/Decimal" }
                borrow_asset:
                  type: string
                  default: USDC
      responses:
        "200":
          description: Transaction built
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionEnvelope" }
              example:
                object: transaction_envelope
                transaction:
                  id: txn_5pR8mK
                  unsigned: "AQABA0pWZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
                  expires_at: "2026-08-24T14:32:30Z"
                simulation:
                  summary: "Borrow 4,222.20 USDC against 45.4 AAPL"
                  effects:
                    - { label: "AAPL pledged", before: "0", after: "45.4" }
                    - { label: "USDC balance", before: "0.00", after: "4,222.20" }
                    - { label: "Health factor", before: "n/a", after: "1.50" }
                    - { label: "Liquidation price", before: "n/a", after: "155.00" }
                  network_fee: "0.000005"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/borrow/loans/{id}:
    get:
      tags: [Borrowing]
      summary: Retrieve a loan
      operationId: getLoan
      parameters:
        - $ref: "#/components/parameters/Version"
        - name: id
          in: path
          required: true
          schema: { type: string, examples: ["loan_8fQz2R"] }
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Loan" }
              example:
                object: loan
                id: loan_8fQz2R
                wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                status: active
                asset: AAPL
                collateral_amount: "45.4"
                collateral_value: "10555.50"
                borrow_asset: USDC
                principal: "4222.20"
                repaid_to_date: "0"
                outstanding: "4222.20"
                interest_accrued: "0"
                current_ltv: "0.40"
                health_factor: "1.50"
                liquidation_price: "155.00"
                available_to_borrow: "1055.55"
                opened_at: "2026-07-11T15:42:00Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/borrow/loans/{id}/repayments:
    post:
      tags: [Borrowing]
      summary: Build a repayment transaction
      description: |
        Partial or full. Set `full: true` to repay the exact outstanding balance without racing accrual,
        which for Spout is only fees since interest is zero.
      operationId: createRepayment
      parameters:
        - $ref: "#/components/parameters/Version"
        - name: id
          in: path
          required: true
          schema: { type: string }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wallet]
              properties:
                wallet: { $ref: "#/components/schemas/WalletAddress" }
                amount: { $ref: "#/components/schemas/Decimal" }
                full:
                  type: boolean
                  default: false
      responses:
        "200":
          description: Transaction built
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionEnvelope" }
              example:
                object: transaction_envelope
                transaction:
                  id: txn_7cX3oJ
                  unsigned: "AQABA0pWZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmYCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
                  expires_at: "2026-08-24T14:32:30Z"
                simulation:
                  summary: "Repay 4,222.20 USDC and release 45.4 AAPL"
                  effects:
                    - { label: "Outstanding", before: "4,222.20", after: "0.00" }
                    - { label: "AAPL released", before: "0", after: "45.4" }
                  network_fee: "0.000005"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/borrow/health:
    get:
      tags: [Borrowing]
      summary: Health factor for a wallet
      description: |
        **Health is tracked per collateral asset, not pooled across the wallet.** A wallet with AAPL and NVDA
        collateral has two independent positions, each with its own health factor and its own liquidation price.
        One can be liquidated while the other is comfortable.

        This differs from pooled-collateral money markets. If your users have used Aave or a margin account,
        surface the per-position health explicitly or they will misread their risk.

        `aggregate` is provided for a portfolio summary line only. Never use it to decide whether a user is safe.
      operationId: getHealth
      parameters:
        - $ref: "#/components/parameters/Version"
        - $ref: "#/components/parameters/Wallet"
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HealthSummary" }
              example:
                object: health_summary
                wallet: "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"
                positions:
                  - loan_id: loan_8fQz2R
                    asset: AAPL
                    health_factor: "1.50"
                    liquidation_price: "155.00"
                    status: healthy
                  - loan_id: loan_9gRa3S
                    asset: MSTR
                    health_factor: "1.06"
                    liquidation_price: "241.10"
                    status: at_risk
                aggregate:
                  total_collateral_value: "24310.75"
                  total_outstanding: "11980.40"
                  weakest_health_factor: "1.06"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/transactions/{id}:
    get:
      tags: [Transactions]
      summary: Retrieve transaction status
      description: |
        Status of a transaction previously returned by a write endpoint. `pending` until you submit and it
        confirms. Terminal states are `confirmed`, `failed` and `expired`.

        A built transaction has a blockhash and therefore an expiry. If the user takes too long to sign,
        it will `expire` and you must rebuild it.
      operationId: getTransaction
      parameters:
        - $ref: "#/components/parameters/Version"
        - name: id
          in: path
          required: true
          schema: { type: string, examples: ["txn_5pR8mK"] }
      responses:
        "200":
          description: OK
          headers:
            Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionStatus" }
              example:
                object: transaction
                id: txn_5pR8mK
                status: confirmed
                signature: "5xY2mQ8vK3nR7pL1tW9jH4bF6cD2aZ8eN5uM3qS7rT1vX9wG4kP2hJ6yB8nC3dV5fA7sL"
                failure_reason: null
                resulting_object:
                  object: loan
                  id: loan_8fQz2R
                created_at: "2026-08-24T14:30:12Z"
                confirmed_at: "2026-08-24T14:30:19Z"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer sk_test_...` for sandbox, `sk_live_...` for production.
        Keys are environment scoped and never interchangeable.

  headers:
    RequestId:
      description: |
        Unique identifier for this request. Present on every response, success or failure. Log it, and quote
        it when you contact support.
      schema: { type: string, examples: ["req_9xKq2mB"] }

  parameters:
    Version:
      name: Spout-Version
      in: header
      required: false
      description: |
        Override the API version pinned to your key for this request. Dated, e.g. `2026-08-24`.
      schema: { type: string }
    Wallet:
      name: wallet
      in: query
      required: true
      description: Solana wallet address, base58.
      schema: { $ref: "#/components/schemas/WalletAddress" }
    Limit:
      name: limit
      in: query
      description: 1 to 100. Defaults to 25.
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    StartingAfter:
      name: starting_after
      in: query
      description: Cursor. Pass the id of the last object from the previous page to walk forward.
      schema: { type: string }
    EndingBefore:
      name: ending_before
      in: query
      description: |
        Cursor. Pass the id of the first object from the current page to walk backward. Mutually exclusive
        with `starting_after`.
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        A unique key you generate, up to 255 characters. Replaying it returns the original response.
        Reusing it with a different body returns 409.
      schema: { type: string, maxLength: 255 }

  responses:
    Unauthorized:
      description: Missing, malformed or revoked API key.
      headers:
        Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: authentication_error
              code: invalid_api_key
              message: "No API key provided. Send it as `Authorization: Bearer sk_test_...`."
              doc_url: "https://spout.finance/docs/errors#invalid_api_key"
              request_id: "req_1aBc2dE"

    BadRequest:
      description: |
        The request is malformed: a missing required field, a wrong type, or a value that does not parse.
        This is a bug in the caller, not a refusal by the protocol.
      headers:
        Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: invalid_request_error
              code: invalid_wallet
              message: "`wallet` is not a valid base58 Solana address."
              param: wallet
              doc_url: "https://spout.finance/docs/errors#invalid_wallet"
              request_id: "req_2bCd3eF"

    NotFound:
      description: No such object.
      headers:
        Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: invalid_request_error
              code: unknown_asset
              message: "No asset with symbol NOPE. Call GET /v1/assets for the supported set."
              doc_url: "https://spout.finance/docs/errors#unknown_asset"
              request_id: "req_3cDe4fG"

    IdempotencyConflict:
      description: |
        The `Idempotency-Key` has already been used with a different request body. Replaying a key with the
        same body is safe and returns the original response; replaying it with a different body means your
        code reused a key it should not have.
      headers:
        Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: invalid_request_error
              code: idempotency_key_reuse
              message: "Idempotency-Key 9f2c1e was already used with a different request body."
              doc_url: "https://spout.finance/docs/errors#idempotency_key_reuse"
              request_id: "req_4dEf5gH"

    RateLimited:
      description: Too many requests. Back off and retry.
      headers:
        Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: rate_limit_error
              code: rate_limited
              message: "Too many requests. Retry after 2 seconds."
              doc_url: "https://spout.finance/docs/errors#rate_limited"
              request_id: "req_5eFg6hI"

    UnprocessableEntity:
      description: |
        Well-formed, but the protocol will not do it. This is the interesting one, and `error.code` tells you
        why. Branch on the code: `exceeds_max_ltv` is a user input problem you can fix in your UI,
        `protocol_paused` and `circuit_breaker_active` are temporary and you should retry later,
        `stale_price_feed` means do not show the user a number you cannot stand behind.

        Prism serves the default example below. To exercise the other codes against the mock, send
        `Prefer: example=<name>`, for example `Prefer: example=protocol_paused`.
      headers:
        Spout-Request-Id: { $ref: "#/components/headers/RequestId" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            exceeds_max_ltv:
              summary: Requested borrow is above the asset's maximum LTV
              value:
                error:
                  type: invalid_request_error
                  code: exceeds_max_ltv
                  message: "Requested borrow of 6000.00 USDC exceeds the maximum of 5277.75 at 50% LTV for AAPL."
                  param: borrow_amount
                  doc_url: "https://spout.finance/docs/errors#exceeds_max_ltv"
                  request_id: "req_9xKq2mB"
            insufficient_collateral:
              summary: Wallet does not hold the collateral it is trying to pledge
              value:
                error:
                  type: invalid_request_error
                  code: insufficient_collateral
                  message: "Wallet holds 12.0 AAPL, cannot pledge 45.4."
                  param: collateral_amount
                  doc_url: "https://spout.finance/docs/errors#insufficient_collateral"
                  request_id: "req_6fGh7iJ"
            asset_not_accepting_deposits:
              summary: Asset is readable but closed to new collateral
              value:
                error:
                  type: protocol_error
                  code: asset_not_accepting_deposits
                  message: "GLD is not accepting new collateral."
                  doc_url: "https://spout.finance/docs/errors#asset_not_accepting_deposits"
                  request_id: "req_7gHi8jK"
            protocol_paused:
              summary: All writes are halted
              value:
                error:
                  type: protocol_error
                  code: protocol_paused
                  message: "The protocol is paused. No new positions can be opened."
                  doc_url: "https://spout.finance/docs/errors#protocol_paused"
                  request_id: "req_8hIj9kL"
            stale_price_feed:
              summary: Oracle price is too old to act on
              value:
                error:
                  type: protocol_error
                  code: stale_price_feed
                  message: "Price for AAPL is 412 seconds old. Not quoting."
                  doc_url: "https://spout.finance/docs/errors#stale_price_feed"
                  request_id: "req_9iJk0lM"

  schemas:

    WalletAddress:
      type: string
      description: Base58 Solana address.
      pattern: "^[1-9A-HJ-NP-Za-km-z]{32,44}$"
      minLength: 32
      maxLength: 44
      examples: ["7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"]

    Decimal:
      type: string
      description: |
        Decimal number as a string. Never a JSON number, to avoid float rounding on 6 to 9 decimal assets.
      pattern: "^-?[0-9]+(\\.[0-9]+)?$"
      examples: ["1250.75"]

    Ratio:
      type: string
      description: |
        A rate, ratio or factor as a decimal string, on the same no-floats rule as Decimal. `"0.50"` is 50%.
        Health factors are also expressed this way and are not bounded to 1: `"1.50"` is a comfortable
        position, anything below `"1.0"` is liquidatable.
      pattern: "^[0-9]+(\\.[0-9]+)?$"
      examples: ["0.50"]

    TrancheId:
      type: string
      enum: [senior, junior]

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, code, message]
          properties:
            type:
              type: string
              enum: [invalid_request_error, authentication_error, rate_limit_error, protocol_error, api_error]
            code:
              type: string
              description: Stable machine-readable identifier. Branch on this, never on `message`.
              enum:
                - invalid_api_key
                - invalid_wallet
                - unknown_asset
                - asset_not_accepting_deposits
                - insufficient_collateral
                - insufficient_balance
                - exceeds_max_ltv
                - below_minimum_amount
                - collateral_already_pledged
                - loan_not_found
                - protocol_paused
                - circuit_breaker_active
                - market_closed
                - stale_price_feed
                - idempotency_key_reuse
                - rate_limited
                - internal_error
            message:
              type: string
              description: Human readable. Safe to log, do not parse.
            param:
              type: string
              description: The offending field, when the error is field-specific.
            doc_url:
              type: string
              format: uri
            request_id:
              type: string

    ProtocolStats:
      type: object
      required: [object, tvl, updated_at]
      properties:
        object: { type: string, const: protocol_stats }
        tvl:
          type: object
          properties:
            total: { $ref: "#/components/schemas/Decimal" }
            senior: { $ref: "#/components/schemas/Decimal" }
            junior: { $ref: "#/components/schemas/Decimal" }
            currency: { type: string, examples: ["USD"] }
        collateral_value: { $ref: "#/components/schemas/Decimal" }
        total_borrowed: { $ref: "#/components/schemas/Decimal" }
        utilization:
          $ref: "#/components/schemas/Ratio"
          description: Fraction of the pool currently lent out. "0.65" is 65%.
        reserve_ratio:
          $ref: "#/components/schemas/Ratio"
          description: Fraction of the pool held in reserve rather than deployed.
        insurance_fund:
          type: object
          properties:
            balance: { $ref: "#/components/schemas/Decimal" }
            coverage_ratio:
              $ref: "#/components/schemas/Ratio"
              description: Insurance fund as a fraction of TVL.
        paused:
          type: boolean
          description: When true, all write endpoints return `protocol_paused`.
        updated_at: { type: string, format: date-time }

    Asset:
      type: object
      required: [object, symbol, name, max_ltv, accepting_deposits]
      properties:
        object: { type: string, const: asset }
        symbol: { type: string, examples: ["AAPL"] }
        name: { type: string, examples: ["Apple Inc."] }
        mint:
          type: string
          description: SPL mint address of the tokenized asset.
        asset_class:
          type: string
          enum: [equity, etf]
        max_ltv:
          $ref: "#/components/schemas/Ratio"
          description: Maximum loan-to-value. "0.50" is 50%.
        liquidation_threshold:
          $ref: "#/components/schemas/Ratio"
          description: LTV at which the position becomes liquidatable.
        estimated_borrow_cost_apr:
          $ref: "#/components/schemas/Ratio"
          description: |
            Interest is 0%. This is the separate, estimated annualised cost a borrower bears when a written
            call finishes in the money and upside above the strike is given up. It is an estimate from
            historical cycles, not a charge, and it is not deducted from the balance. Show it alongside
            the 0% rate, never instead of it.
        cycle:
          type: string
          enum: [weekly, biweekly]
        price:
          type: object
          properties:
            value: { $ref: "#/components/schemas/Decimal" }
            currency: { type: string }
            as_of: { type: string, format: date-time }
            stale: { type: boolean }
        accepting_deposits: { type: boolean }
        decimals: { type: integer, examples: [9] }

    Tranche:
      type: object
      required: [object, id, estimated_apy]
      properties:
        object: { type: string, const: tranche }
        id: { $ref: "#/components/schemas/TrancheId" }
        display_name: { type: string, examples: ["Senior"] }
        estimated_apy:
          $ref: "#/components/schemas/Ratio"
          description: Estimate from trailing realised cycles. Not a guarantee. Label it as an estimate.
        loss_position:
          type: integer
          description: Order in the loss waterfall. Lower absorbs losses earlier.
        exit:
          type: object
          properties:
            instant_available: { type: boolean }
            instant_haircut_range:
              type: array
              items: { $ref: "#/components/schemas/Ratio" }
              description: Min and max haircut as decimal strings, e.g. ["0", "0.03"].
            queue_notice_days: { type: integer }
        capacity:
          type: object
          properties:
            deposited: { $ref: "#/components/schemas/Decimal" }
            cap: { $ref: "#/components/schemas/Decimal" }
        minimum_deposit: { $ref: "#/components/schemas/Decimal" }

    LendPosition:
      type: object
      required: [object, id, tranche, balance]
      properties:
        object: { type: string, const: lend_position }
        id: { type: string, examples: ["lp_2Kf9wQ"] }
        wallet: { $ref: "#/components/schemas/WalletAddress" }
        tranche: { $ref: "#/components/schemas/TrancheId" }
        shares: { $ref: "#/components/schemas/Decimal" }
        balance:
          $ref: "#/components/schemas/Decimal"
          description: Current redeemable value at NAV, before any instant-exit haircut.
        principal_deposited: { $ref: "#/components/schemas/Decimal" }
        yield_earned_to_date: { $ref: "#/components/schemas/Decimal" }
        pending_withdrawal_id:
          type: [string, "null"]
        created_at: { type: string, format: date-time }

    WithdrawalPreview:
      type: object
      required: [object, mode, amount, receivable]
      properties:
        object: { type: string, const: withdrawal_preview }
        mode: { type: string, enum: [instant, queue] }
        amount: { $ref: "#/components/schemas/Decimal" }
        haircut:
          $ref: "#/components/schemas/Ratio"
          description: Applied only for instant senior exits. "0.015" is 1.5%.
        receivable:
          $ref: "#/components/schemas/Decimal"
          description: What the user actually receives after any haircut.
        estimated_available_at:
          type: [string, "null"]
          format: date-time
          description: For queued exits. Null when it cannot be estimated.

    WithdrawalRequest:
      type: object
      required: [object, id, status]
      properties:
        object: { type: string, const: withdrawal_request }
        id: { type: string, examples: ["wdr_3kQ1vN"] }
        wallet: { $ref: "#/components/schemas/WalletAddress" }
        tranche: { $ref: "#/components/schemas/TrancheId" }
        status:
          type: string
          enum: [queued, partially_filled, filled, cancelled]
        amount_requested: { $ref: "#/components/schemas/Decimal" }
        amount_filled: { $ref: "#/components/schemas/Decimal" }
        queue_position: { type: [integer, "null"] }
        estimated_available_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }

    YieldDistribution:
      type: object
      required: [object, id, cycle_id, amount, distributed_at]
      properties:
        object: { type: string, const: yield_distribution }
        id: { type: string, examples: ["yd_7Ln4xT"] }
        wallet: { $ref: "#/components/schemas/WalletAddress" }
        tranche: { $ref: "#/components/schemas/TrancheId" }
        cycle_id: { type: string, examples: ["cyc_2026_34"] }
        amount: { $ref: "#/components/schemas/Decimal" }
        realized_apy:
          $ref: "#/components/schemas/Ratio"
          description: Annualised rate implied by this single cycle.
        distributed_at: { type: string, format: date-time }

    BorrowPowerQuote:
      type: object
      required: [object, asset, max_borrowable]
      properties:
        object: { type: string, const: borrow_power_quote }
        wallet: { $ref: "#/components/schemas/WalletAddress" }
        asset: { type: string }
        collateral_amount: { $ref: "#/components/schemas/Decimal" }
        collateral_value: { $ref: "#/components/schemas/Decimal" }
        max_ltv: { $ref: "#/components/schemas/Ratio" }
        max_borrowable: { $ref: "#/components/schemas/Decimal" }
        already_pledged: { $ref: "#/components/schemas/Decimal" }
        projected:
          type: object
          description: What the position would look like at max borrow.
          properties:
            health_factor: { $ref: "#/components/schemas/Ratio" }
            liquidation_price: { $ref: "#/components/schemas/Decimal" }
        quoted_at: { type: string, format: date-time }
        expires_at:
          type: string
          format: date-time
          description: Quotes are indicative. Past this point, re-quote.

    Loan:
      type: object
      required: [object, id, status, asset, outstanding, health_factor]
      properties:
        object: { type: string, const: loan }
        id: { type: string, examples: ["loan_8fQz2R"] }
        wallet: { $ref: "#/components/schemas/WalletAddress" }
        status:
          type: string
          enum: [active, at_risk, repaying, closed, liquidated]
        asset: { type: string }
        collateral_amount: { $ref: "#/components/schemas/Decimal" }
        collateral_value: { $ref: "#/components/schemas/Decimal" }
        borrow_asset:
          type: string
          description: The stablecoin drawn. Same field name as the request body on POST /v1/borrow/loans.
        principal: { $ref: "#/components/schemas/Decimal" }
        repaid_to_date: { $ref: "#/components/schemas/Decimal" }
        outstanding: { $ref: "#/components/schemas/Decimal" }
        interest_accrued:
          $ref: "#/components/schemas/Decimal"
          description: Always "0". Present so partners can render an interest line that reads zero.
        current_ltv: { $ref: "#/components/schemas/Ratio" }
        health_factor:
          $ref: "#/components/schemas/Ratio"
          description: Below 1.0 is liquidatable. Health is per loan, not per wallet.
        liquidation_price: { $ref: "#/components/schemas/Decimal" }
        available_to_borrow: { $ref: "#/components/schemas/Decimal" }
        opened_at: { type: string, format: date-time }

    HealthSummary:
      type: object
      required: [object, positions]
      properties:
        object: { type: string, const: health_summary }
        wallet: { $ref: "#/components/schemas/WalletAddress" }
        positions:
          type: array
          description: One entry per collateral asset. These are independent.
          items:
            type: object
            properties:
              loan_id: { type: string }
              asset: { type: string }
              health_factor: { $ref: "#/components/schemas/Ratio" }
              liquidation_price: { $ref: "#/components/schemas/Decimal" }
              status: { type: string, enum: [healthy, warning, at_risk, liquidatable] }
        aggregate:
          type: object
          description: |
            Portfolio summary for display only. Do not use `weakest_health_factor` as a safety signal for the
            whole wallet, and do not compute a pooled health factor from these figures. Positions do not
            cross-collateralise.
          properties:
            total_collateral_value: { $ref: "#/components/schemas/Decimal" }
            total_outstanding: { $ref: "#/components/schemas/Decimal" }
            weakest_health_factor: { $ref: "#/components/schemas/Ratio" }

    TransactionEnvelope:
      type: object
      required: [object, transaction]
      properties:
        object: { type: string, const: transaction_envelope }
        transaction:
          type: object
          required: [id, unsigned, expires_at]
          properties:
            id: { type: string, examples: ["txn_5pR8mK"] }
            unsigned:
              type: string
              description: Base64 serialized Solana transaction. Have the user sign this, then submit it.
              contentEncoding: base64
            expires_at:
              type: string
              format: date-time
              description: Derived from the recent blockhash. Rebuild after this.
        simulation:
          type: object
          description: |
            What this transaction will do, in terms you can show a user before they sign. Present this rather
            than raw instruction data.
          properties:
            summary: { type: string, examples: ["Borrow 5,000.00 USDC against 45.4 AAPL"] }
            effects:
              type: array
              items:
                type: object
                properties:
                  label: { type: string }
                  before: { type: string }
                  after: { type: string }
            network_fee: { $ref: "#/components/schemas/Decimal" }

    WebhookEvent:
      type: object
      required: [id, type, created_at, data]
      description: |
        The body POSTed to your webhook endpoint. Delivery is at-least-once and ordering is not guaranteed,
        so handlers must be idempotent on `id` and must not assume they see events in sequence.
      properties:
        id:
          type: string
          description: Unique per event. Deduplicate on this.
          examples: ["evt_4mT8pL"]
        type:
          type: string
          enum:
            - loan.at_risk
            - loan.liquidated
            - cycle.settled
            - withdrawal.available
            - transaction.confirmed
        api_version:
          type: string
          description: The version this payload was serialized under.
          examples: ["2026-08-24"]
        created_at: { type: string, format: date-time }
        data:
          type: object
          description: |
            The affected object. Its shape follows `type`: a Loan for loan events, a WithdrawalRequest for
            withdrawal events, a TransactionStatus for transaction events, and a cycle summary for
            `cycle.settled`.
          properties:
            object:
              type: string
              description: Discriminator matching the `object` field of the payload schema.
          additionalProperties: true
      examples:
        - id: "evt_4mT8pL"
          type: "loan.at_risk"
          api_version: "2026-08-24"
          created_at: "2026-08-24T14:31:02Z"
          data:
            object: "loan"
            id: "loan_8fQz2R"
            asset: "AAPL"
            health_factor: "1.08"
            liquidation_price: "182.40"

    TransactionStatus:
      type: object
      required: [object, id, status]
      properties:
        object: { type: string, const: transaction }
        id: { type: string }
        status:
          type: string
          enum: [pending, submitted, confirmed, failed, expired]
        signature:
          type: [string, "null"]
          description: Solana transaction signature once submitted.
        failure_reason: { type: [string, "null"] }
        resulting_object:
          type: [object, "null"]
          description: The loan, position or withdrawal created, once confirmed.
        created_at: { type: string, format: date-time }
        confirmed_at: { type: [string, "null"], format: date-time }

webhooks:
  loanAtRisk:
    post:
      operationId: webhookLoanAtRisk
      tags: [Webhooks]
      security: []
      parameters:
        - name: Spout-Signature
          in: header
          required: true
          description: |
            HMAC signature over the raw request body. Verify it before trusting anything in the payload.
          schema: { type: string }
      summary: loan.at_risk
      description: |
        A loan's health factor has crossed a warning threshold. Fires at each configured threshold, not
        continuously. This is the event that matters most to your users, so wire it before the others.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200": { description: Acknowledged }

  loanLiquidated:
    post:
      operationId: webhookLoanLiquidated
      tags: [Webhooks]
      security: []
      parameters:
        - name: Spout-Signature
          in: header
          required: true
          description: |
            HMAC signature over the raw request body. Verify it before trusting anything in the payload.
          schema: { type: string }
      summary: loan.liquidated
      description: Collateral was partially or fully liquidated. Payload states what was sold and what remains.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200": { description: Acknowledged }

  cycleSettled:
    post:
      operationId: webhookCycleSettled
      tags: [Webhooks]
      security: []
      parameters:
        - name: Spout-Signature
          in: header
          required: true
          description: |
            HMAC signature over the raw request body. Verify it before trusting anything in the payload.
          schema: { type: string }
      summary: cycle.settled
      description: An options cycle settled and yield was distributed to lenders.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200": { description: Acknowledged }

  withdrawalAvailable:
    post:
      operationId: webhookWithdrawalAvailable
      tags: [Webhooks]
      security: []
      parameters:
        - name: Spout-Signature
          in: header
          required: true
          description: |
            HMAC signature over the raw request body. Verify it before trusting anything in the payload.
          schema: { type: string }
      summary: withdrawal.available
      description: A queued withdrawal is now claimable.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200": { description: Acknowledged }

  transactionConfirmed:
    post:
      operationId: webhookTransactionConfirmed
      tags: [Webhooks]
      security: []
      parameters:
        - name: Spout-Signature
          in: header
          required: true
          description: |
            HMAC signature over the raw request body. Verify it before trusting anything in the payload.
          schema: { type: string }
      summary: transaction.confirmed
      description: A transaction you submitted confirmed on chain. Carries the resulting object.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200": { description: Acknowledged }
