openapi: 3.0.3
info:
  title: Curtain Developer API
  version: 1.0.0
  description: >-
    V2, V3, or dynamic private swap preparation on Robinhood Chain. Call from your server.
    API keys do not sign or broadcast transactions. Users retain wallet custody.
    Obtain a key in https://curtainrh.com/app/developer.
servers:
  - url: https://operator.curtainrh.com/v1
security:
  - bearerAuth: []
paths:
  /keeper/v1/settlements/pending:
    get:
      operationId: getPendingSettlements
      summary: Public signed settlements for permissionless keepers
      security: []
      servers:
        - url: https://operator.curtainrh.com
      parameters:
        - {
            name: privacyRoute,
            in: query,
            required: true,
            schema: { type: string, enum: [v2, v3] },
          }
      responses:
        "200":
          description: Settlements that any funded wallet may submit to the returned vault
          content:
            application/json:
              schema:
                type: object
                required: [chainId, privacyRoute, vault, settlements]
                properties:
                  chainId: { type: integer, example: 4663 }
                  privacyRoute: { type: string, enum: [v2, v3] }
                  vault: { $ref: "#/components/schemas/Address" }
                  settlements:
                    type: array
                    items: { $ref: "#/components/schemas/PendingSettlement" }
        default: { $ref: "#/components/responses/Error" }
  /config:
    get:
      operationId: getConfig
      summary: Network, vault, token addresses, and limits
      responses:
        "200":
          description: V2 configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  chainId: { type: integer, example: 4663 }
                  privacyRoutes: { type: array, items: { type: string, enum: [v2, v3, dynamic] } }
                  vault: { $ref: "#/components/schemas/Address" }
                  vaults:
                    { type: object, additionalProperties: { $ref: "#/components/schemas/Address" } }
                  tokens:
                    { type: object, additionalProperties: { $ref: "#/components/schemas/Address" } }
                  maxDelaySeconds: { type: integer, example: 15552000 }
                  rateLimitPerMinute: { type: integer, example: 60 }
                  privacyPolicy:
                    type: object
                    properties:
                      dynamic:
                        type: object
                        properties:
                          priority: { type: array, items: { type: string, enum: [v3, v2] } }
                          fallback: { type: string, enum: [v2] }
                          rule: { type: string }
        default: { $ref: "#/components/responses/Error" }
  /quote:
    get:
      operationId: getQuote
      summary: Quote a private swap in raw token units
      parameters:
        - {
            name: privacyRoute,
            in: query,
            required: true,
            schema: { type: string, enum: [v2, v3, dynamic] },
          }
        - {
            name: tokenIn,
            in: query,
            required: true,
            schema: { $ref: "#/components/schemas/Address" },
          }
        - {
            name: tokenOut,
            in: query,
            required: true,
            schema: { $ref: "#/components/schemas/Address" },
          }
        - {
            name: amountIn,
            in: query,
            required: true,
            schema: { $ref: "#/components/schemas/PositiveUnits" },
          }
        - {
            name: slippageBps,
            in: query,
            schema: { type: integer, minimum: 0, maximum: 5000, default: 100 },
          }
        - {
            name: integratorFeeBps,
            in: query,
            schema: { type: integer, minimum: 0, maximum: 100 },
            description: Optional integrator fee in basis points; pair with integratorFeeRecipient.
          }
        - {
            name: integratorFeeRecipient,
            in: query,
            schema: { $ref: "#/components/schemas/Address" },
            description: Recipient of the optional integrator fee.
          }
      responses:
        "200":
          description: Check available before using minOutSuggested; quotes do not reserve a price
          content:
            application/json:
              schema:
                type: object
                properties:
                  available: { type: boolean }
                  amountIn: { type: string }
                  marketOut: { type: string }
                  expectedOut: { type: string }
                  minOutSuggested: { type: string }
                  protocolFee: { type: string }
                  keeperFee: { type: string }
                  integratorFee:
                    type: object
                    properties:
                      recipient: { $ref: "#/components/schemas/Address" }
                      bps: { type: integer, minimum: 0, maximum: 100 }
                      amount: { type: string }
                  venue: { type: string }
                  requestedPrivacyRoute: { type: string, enum: [v2, v3, dynamic] }
                  privacyRoute: { type: string, enum: [v2, v3] }
                  routeReason:
                    type: string
                    enum: [explicit_v2, explicit_v3, approved_fixed_denomination, amount_not_in_v3_denomination_set]
        default: { $ref: "#/components/responses/Error" }
  /intents:
    post:
      operationId: createIntent
      summary: Create an intent and prepare unsigned approval/deposit transactions
      description: >-
        Save escapeTicket before asking the wallet to deposit. Identical retries return the
        same intent and original deadline. Different parameters for the same idempotency key
        return 409. Idempotency does not protect against broadcasting duplicate deposits.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_.:-]+$" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [privacyRoute, tokenIn, tokenOut, amountIn, minOut, depositor, recipient]
              properties:
                privacyRoute: { type: string, enum: [v2, v3, dynamic] }
                tokenIn: { $ref: "#/components/schemas/Address" }
                tokenOut: { $ref: "#/components/schemas/Address" }
                amountIn: { $ref: "#/components/schemas/PositiveUnits" }
                minOut: { $ref: "#/components/schemas/PositiveUnits" }
                depositor: { $ref: "#/components/schemas/Address" }
                recipient: { $ref: "#/components/schemas/Address" }
                integratorFee:
                  type: object
                  required: [recipient, bps]
                  properties:
                    recipient: { $ref: "#/components/schemas/Address" }
                    bps: { type: integer, minimum: 0, maximum: 100 }
                delaySeconds: { type: integer, minimum: 0, maximum: 15552000, default: 0 }
                orderType: { type: string, enum: [market, limit], default: market }
                expiresInSeconds:
                  type: integer
                  minimum: 60
                  maximum: 15552000
                  description: Required for limit orders; the order waits for minOut until this expiry.
      responses:
        "201":
          description: Intent created; nothing has been broadcast
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PreparedIntent" }
        "200":
          description: Existing intent returned for an identical retry
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PreparedIntent" }
        default: { $ref: "#/components/responses/Error" }
  /intents/{id}:
    get:
      operationId: getIntent
      summary: Track an intent created by this API key
      parameters:
        - {
            name: id,
            in: path,
            required: true,
            schema: { type: string, pattern: "^[a-f0-9]{32}$" },
          }
      responses:
        "200":
          description: Current intent state; fields may be null until available
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  status:
                    type: string
                    enum:
                      [
                        awaiting_deposit,
                        deposited,
                        settling,
                        paid,
                        blocked,
                        expired,
                        refund_requested,
                        refunded,
                        challenged,
                      ]
                  orderType: { type: string, enum: [market, limit] }
                  deadline: { type: string }
                  depositId: { type: string, nullable: true }
                  amountOut: { type: string, nullable: true }
                  payoutTx: { type: string, nullable: true }
                  blockedReason: { type: string, nullable: true }
        default: { $ref: "#/components/responses/Error" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Server-side API key generated in the Curtain dashboard
  responses:
    Error:
      description: >-
        400 invalid input; 401 missing/invalid/revoked key; 404 missing or unowned intent;
        409 idempotency conflict; 413 body exceeds 16 KB; 429 rate limited (60 requests/min/key);
        503 temporarily unavailable. Responses use Cache-Control no-store.
      headers:
        Retry-After:
          description: Seconds to wait, included for rate-limit errors
          schema: { type: integer }
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error: { type: string }
  schemas:
    Address:
      type: string
      pattern: "^0x[a-fA-F0-9]{40}$"
    PositiveUnits:
      type: string
      pattern: "^[1-9][0-9]{0,77}$"
      description: Positive uint256 decimal string, less than 2^256
    Transaction:
      type: object
      required: [chainId, from, to, data, value]
      properties:
        chainId: { type: integer }
        from: { $ref: "#/components/schemas/Address" }
        to: { $ref: "#/components/schemas/Address" }
        data: { type: string, pattern: "^0x[a-fA-F0-9]*$" }
        value: { type: string, enum: ["0"] }
    PreparedIntent:
      type: object
      required: [id, privacyRoute, chainId, vault, escapeTicket, transactions]
      properties:
        id: { type: string }
        requestedPrivacyRoute: { type: string, enum: [v2, v3, dynamic] }
        privacyRoute: { type: string, enum: [v2, v3] }
        routeReason:
          type: string
          enum: [explicit_v2, explicit_v3, approved_fixed_denomination, amount_not_in_v3_denomination_set]
        chainId: { type: integer }
        vault: { $ref: "#/components/schemas/Address" }
        escapeTicket:
          type: object
          required: [vault, deadline, salt, deadlineHash]
          description: Save before deposit; append depositId from the Deposited event afterward. V3 includes tag.
          properties:
            vault: { $ref: "#/components/schemas/Address" }
            deadline: { type: string }
            salt: { type: string }
            deadlineHash: { type: string }
            tag: { type: string, nullable: true, description: Required by the V3 refund function }
        transactions:
          type: object
          properties:
            approval: { $ref: "#/components/schemas/Transaction" }
            deposit: { $ref: "#/components/schemas/Transaction" }
    PendingSettlement:
      type: object
      required: [swap, payouts, deadline, nonce, signature]
      properties:
        swap:
          type: object
          required: [router, tokenIn, amountIn, tokenOut, minOut, data]
          properties:
            router: { $ref: "#/components/schemas/Address" }
            tokenIn: { $ref: "#/components/schemas/Address" }
            amountIn: { type: string }
            tokenOut: { $ref: "#/components/schemas/Address" }
            minOut: { type: string }
            data: { type: string, pattern: "^0x[a-fA-F0-9]*$" }
        payouts:
          type: array
          items:
            type: object
            required: [recipient, amount, protocolFee, keeperFee, tag]
            properties:
              recipient: { $ref: "#/components/schemas/Address" }
              amount: { type: string }
              protocolFee: { type: string }
              keeperFee: { type: string }
              tag: { type: string }
        deadline: { type: string }
        nonce: { type: string }
        signature: { type: string, pattern: "^0x[a-fA-F0-9]*$" }
