{
  "openapi": "3.1.0",
  "info": {
    "title": "Textile RFQ API (v2)",
    "version": "2.0.0",
    "description": "Request-for-quote: you name a pair, an exact size, and the wallet\nthat will settle. Textile asks the market makers online on that\ncorridor to price it, then returns one firm quote you can execute\n(one maker, or several bundled). By default that is every maker on\nthe corridor; preferred or restricted wallets narrow who is asked.\nIf nobody covers the full amount, you get a clear no-quote. The\npublic Swap page uses this same request.\nCredentials are optional: present a partner API key, or call\nanonymously with a `taker` wallet. Either way, keep the `claimToken`\nthat /rfq/request returns and send it as `X-Rfq-Claim` on cancel,\nsubmit, and status. That token is the ownership proof, not the RFQ\nid, which every solicited maker receives and which authorizes nothing\non its own.\n\nMakers connect over `WSS /v2/maker/stream` with a maker credential and\nan EIP-712 session challenge."
  },
  "servers": [
    {
      "url": "https://api.textilecredit.com/v2"
    },
    {
      "url": "http://localhost:10000/v2",
      "description": "Local dev"
    }
  ],
  "security": [
    {},
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "System"
    },
    {
      "name": "Maker",
      "description": "Wallet-signed enroll and the maker stream"
    },
    {
      "name": "RFQ",
      "description": "Solicit firm quotes and settle them"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Readiness probe",
        "description": "Whether this task can answer a quote request: it either holds the venue engine lease or knows a holder to forward to. 503 means /v2/rfq/* would fail from here, so the ALB keeps the task out of rotation.",
        "security": [],
        "responses": {
          "200": {
            "description": "Venue is serviceable from this task"
          },
          "503": {
            "description": "No venue engine elected or discovered"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "This document",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI document"
          }
        }
      }
    },
    "/maker/enroll": {
      "post": {
        "tags": [
          "Maker"
        ],
        "summary": "Enroll a maker by wallet signature",
        "security": [],
        "description": "The bot signs an EIP-712 MakerEnroll with its funding wallet. First connect creates the maker and returns a key. It does not enable any corridor — that happens when the operator confirms the email address they gave. Re-enroll on the same chain rotates only that key slot and returns corridors already enabled. `flagged` is true when Textile has blocked the maker — they get a key but no fan-out. The plaintext API key is returned once.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrollMakerRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Maker credential and corridor list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnrollMakerResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/maker/verify-email": {
      "post": {
        "tags": [
          "Maker"
        ],
        "summary": "Give Textile an email address and get the confirm link",
        "description": "Maker credential required. The only setup step: confirming the address seats this maker on every RFQ corridor on every chain, now and as more are listed. Posting the same address again resends the link; posting a different one moves where we write to you.",
        "security": [
          {
            "makerKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contactEmail"
                ],
                "properties": {
                  "contactEmail": {
                    "type": "string",
                    "format": "email",
                    "minLength": 1,
                    "maxLength": 254,
                    "description": "An inbox you own. Placeholder and throwaway domains (example.com, mailinator, …) are refused; the rest is proven by the confirm link."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation link sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MakerVerifyEmailSent"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/maker/status": {
      "get": {
        "tags": [
          "Maker"
        ],
        "summary": "Verification state and enabled corridors",
        "description": "What the Stitch panel polls after Connect. `corridors` lists the seats on this key's chain and stays empty until `emailVerified` is true; `flagged` true means Textile has blocked this maker.",
        "security": [
          {
            "makerKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current verification state and seats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MakerStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/maker/access-request": {
      "post": {
        "tags": [
          "Maker"
        ],
        "summary": "Deprecated — use POST /maker/verify-email",
        "deprecated": true,
        "description": "Use POST /maker/verify-email. Behaves identically — submits the address and mails the confirm link — and additionally accepts and ignores `contactWhatsapp`, `note` and `corridor`. `accessStatus` is APPROVED once the address is confirmed, PENDING while it is not.",
        "security": [
          {
            "makerKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contactEmail"
                ],
                "properties": {
                  "contactEmail": {
                    "type": "string",
                    "format": "email",
                    "minLength": 1,
                    "maxLength": 254
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmation link sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyMakerAccessAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/maker/access-status": {
      "get": {
        "tags": [
          "Maker"
        ],
        "summary": "Deprecated — use GET /maker/status",
        "deprecated": true,
        "description": "Use GET /maker/status. Returns the same seats plus an `accessStatus` field: APPROVED once the address is confirmed, PENDING while an address is on file but unconfirmed, NONE before one is given. REJECTED is never returned.",
        "security": [
          {
            "makerKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current verification state and seats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyMakerAccessStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/rfq/preview": {
      "post": {
        "tags": [
          "RFQ"
        ],
        "summary": "Preview a quote from published levels",
        "description": "Unlocked estimate from the makers' latest published levels. Does not solicit, sign, reserve inventory, or take an outstanding RFQ slot. Amounts can be a few seconds stale. Use POST /rfq/request when the taker commits to a firm, executable quote.",
        "security": [
          {},
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRfqRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Indicative preview or no-quote result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RfqPreviewOutcome"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/rfq/request": {
      "post": {
        "tags": [
          "RFQ"
        ],
        "summary": "Request a firm quote",
        "description": "No credential required — `taker` is, plus `takerProof`, a short-lived EIP-712 signature by that taker wallet proving the caller controls it (partner keys and admin sessions are exempt). A partner key needs `trades:write`. `preferredLiquidityWallets` / `restrictedLiquidityWallets` are honoured without a credential (a silent drop would price a route the caller did not ask for). Keep the returned `claimToken` — it is what authorizes cancel, submit, and status for this RFQ, and it is returned exactly once. Blocks up to the corridor reply budget (~750ms pilot) while makers are solicited. Returns a firm, taker-bound signed quote with ready-to-broadcast calldata, or a no-quote result with a stable reason. A firm quote lives for the corridor quote TTL (60s default); `quote.expiresAt` is the accept cutoff and may be sooner. `taker` is required. `preferredLiquidityWallets` / `restrictedLiquidityWallets`: max 10 each, deduped, mutually exclusive (both → 400); a restricted miss returns reason `no_restricted_liquidity`, never an open-market substitute.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CreateRfqRequest"
                  },
                  {
                    "type": "object",
                    "required": [
                      "taker"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Firm quote or no-quote result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RfqOutcome"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/rfq/{id}/cancel": {
      "post": {
        "tags": [
          "RFQ"
        ],
        "summary": "Release a firm quote you requested",
        "description": "Un-counts this RFQ's live inventory and tells the selected maker (`quoteExpired`). Only the caller that created the quote can cancel it, and only while it is still quoted. A foreign id is 404. Does not un-sign Permit2 — the outstanding-RFQ slot stays held until the signed order deadline so a caller cannot stack more live orders than the cap by cancelling.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rfq-Claim",
            "in": "header",
            "required": false,
            "description": "The claimToken from POST /rfq/request. Required for a caller with no partner key; accepted from any caller as proof it created this RFQ.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inventory released",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "rfqId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "released"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/rfq/{id}/submit": {
      "post": {
        "tags": [
          "RFQ"
        ],
        "summary": "Report the settlement transaction for a firm quote",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rfq-Claim",
            "in": "header",
            "required": false,
            "description": "The claimToken from POST /rfq/request. Required for a caller with no partner key; accepted from any caller as proof it created this RFQ.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "txHash"
                ],
                "properties": {
                  "txHash": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{64}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated RFQ status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RfqStatusEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/rfq/{id}": {
      "get": {
        "tags": [
          "RFQ"
        ],
        "summary": "RFQ status",
        "description": "Status, routing summary, and terminal fields. The signed order is returned exactly once, by /rfq/request — never replayed here.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rfq-Claim",
            "in": "header",
            "required": false,
            "description": "The claimToken from POST /rfq/request. Required for a caller with no partner key; accepted from any caller as proof it created this RFQ.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "RFQ status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RfqStatusEnvelope"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Optional. Partner API key (tx_test_/tx_live_...), or a Textile wallet-session JWT. Omit it to call anonymously against a `taker` wallet. Taker paths only — a maker credential is not accepted here."
      },
      "makerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Maker credential issued once by POST /v2/maker/enroll. Bound to one maker and one chain, and distinct from a partner API key — a partner key or wallet session is rejected on the maker paths."
      }
    },
    "schemas": {
      "UnsignedTransaction": {
        "type": "object",
        "description": "An unsigned EVM transaction to sign and broadcast.",
        "required": [
          "to",
          "data",
          "value",
          "chainId"
        ],
        "properties": {
          "to": {
            "type": "string",
            "description": "Approval: the sell token. Swap: the reactor from the quote."
          },
          "data": {
            "type": "string",
            "description": "ABI-encoded calldata, 0x-hex."
          },
          "value": {
            "type": "string",
            "description": "wei as a decimal string; always \"0\" for FX swaps"
          },
          "chainId": {
            "type": "integer"
          }
        }
      },
      "EnrollMakerRequest": {
        "type": "object",
        "required": [
          "chainId",
          "signingAddress",
          "issuedAt",
          "signature"
        ],
        "properties": {
          "chainId": {
            "type": "integer"
          },
          "signingAddress": {
            "type": "string"
          },
          "fundingWallet": {
            "type": "string",
            "description": "OperatorVault address when the signer is a strategy EOA. Bound in the MakerEnroll EIP-712 digest (equals signingAddress when omitted)."
          },
          "issuedAt": {
            "type": "integer",
            "description": "Unix milliseconds at signing time"
          },
          "signature": {
            "type": "string",
            "description": "EIP-712 MakerEnroll hex"
          }
        }
      },
      "EnrollMakerResponse": {
        "type": "object",
        "required": [
          "makerId",
          "makerSlug",
          "environment",
          "apiKey",
          "streamUrl",
          "corridors",
          "corridorPairs",
          "flagged"
        ],
        "properties": {
          "makerId": {
            "type": "string"
          },
          "makerSlug": {
            "type": "string"
          },
          "environment": {
            "type": "string",
            "enum": [
              "LIVE",
              "TEST"
            ]
          },
          "apiKey": {
            "type": "string",
            "description": "Returned once. Never logged."
          },
          "streamUrl": {
            "type": "string"
          },
          "validationContract": {
            "type": "string",
            "nullable": true
          },
          "corridors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "RFQ-capable corridor slugs already enabled on this chain. Empty until the operator confirms their email address."
          },
          "corridorPairs": {
            "type": "array",
            "description": "Same seats as `corridors`, with token addresses so a custom (non-catalog) bot can bind its pool.",
            "items": {
              "$ref": "#/components/schemas/CorridorPair"
            }
          },
          "flagged": {
            "type": "boolean",
            "description": "True when Textile has blocked this maker. They are not solicited until unblocked."
          }
        }
      },
      "CorridorPair": {
        "type": "object",
        "description": "One RFQ-capable seat, with the tokens a bot binds to.",
        "required": [
          "slug",
          "chainId",
          "collateralToken",
          "debtToken"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "chainId": {
            "type": "integer"
          },
          "collateralToken": {
            "type": "string"
          },
          "debtToken": {
            "type": "string"
          }
        }
      },
      "MakerVerifyEmailSent": {
        "type": "object",
        "description": "The link is on its way. Nothing is seated until it is clicked.",
        "required": [
          "contactEmail",
          "emailVerified",
          "sent"
        ],
        "properties": {
          "contactEmail": {
            "type": "string"
          },
          "emailVerified": {
            "type": "boolean",
            "description": "True only when this address was already confirmed on an earlier link."
          },
          "sent": {
            "type": "boolean",
            "description": "False when the address was already confirmed, so no new mail went out."
          }
        }
      },
      "MakerStatus": {
        "type": "object",
        "required": [
          "emailVerified",
          "flagged",
          "makerId",
          "makerSlug",
          "environment",
          "streamUrl",
          "corridors",
          "corridorPairs"
        ],
        "properties": {
          "emailVerified": {
            "type": "boolean",
            "description": "True once the operator clicked the confirm link. This is the whole access gate."
          },
          "contactEmail": {
            "type": "string",
            "nullable": true
          },
          "flagged": {
            "type": "boolean",
            "description": "True when Textile has blocked this maker. Seats read empty while set."
          },
          "makerId": {
            "type": "string"
          },
          "makerSlug": {
            "type": "string"
          },
          "environment": {
            "type": "string",
            "enum": [
              "LIVE",
              "TEST"
            ]
          },
          "streamUrl": {
            "type": "string"
          },
          "validationContract": {
            "type": "string",
            "nullable": true
          },
          "corridors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "RFQ-capable corridor slugs enabled on this chain. Empty until the email is confirmed."
          },
          "corridorPairs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CorridorPair"
            }
          }
        }
      },
      "LegacyMakerAccessAccepted": {
        "type": "object",
        "deprecated": true,
        "required": [
          "accessStatus",
          "requestId",
          "contactEmail",
          "emailVerified"
        ],
        "properties": {
          "accessStatus": {
            "type": "string",
            "enum": [
              "PENDING",
              "APPROVED"
            ]
          },
          "requestId": {
            "type": "string",
            "description": "The maker id. Access is keyed on the maker now; there is no request row."
          },
          "contactEmail": {
            "type": "string"
          },
          "emailVerified": {
            "type": "boolean"
          }
        }
      },
      "LegacyMakerAccessStatus": {
        "type": "object",
        "deprecated": true,
        "required": [
          "accessStatus",
          "emailVerified",
          "flagged",
          "makerId",
          "makerSlug",
          "environment",
          "streamUrl",
          "corridors",
          "corridorPairs"
        ],
        "properties": {
          "accessStatus": {
            "type": "string",
            "enum": [
              "NONE",
              "PENDING",
              "APPROVED"
            ],
            "description": "Derived from emailVerified: APPROVED once confirmed, otherwise PENDING or NONE."
          },
          "emailVerified": {
            "type": "boolean"
          },
          "contactEmail": {
            "type": "string",
            "nullable": true
          },
          "flagged": {
            "type": "boolean"
          },
          "makerId": {
            "type": "string"
          },
          "makerSlug": {
            "type": "string"
          },
          "environment": {
            "type": "string",
            "enum": [
              "LIVE",
              "TEST"
            ]
          },
          "streamUrl": {
            "type": "string"
          },
          "validationContract": {
            "type": "string",
            "nullable": true
          },
          "corridors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "corridorPairs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CorridorPair"
            }
          }
        }
      },
      "RfqStatusEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/RfqStatus"
          }
        }
      },
      "RfqStatus": {
        "type": "object",
        "description": "Never carries the signed order: encodedOrder/signature are returned exactly once, by POST /rfq/request.",
        "properties": {
          "rfqId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "soliciting",
              "quoted",
              "no_quote",
              "submitted",
              "filled",
              "failed",
              "expired"
            ]
          },
          "chainId": {
            "type": "integer"
          },
          "sellToken": {
            "type": "string"
          },
          "buyToken": {
            "type": "string"
          },
          "sellAmount": {
            "type": "string",
            "nullable": true,
            "description": "Once a winner exists, the settled sell-token debit (order output + fee) rather than the requested cap."
          },
          "buyAmount": {
            "type": "string",
            "nullable": true,
            "description": "Once a winner exists, the amount actually delivered."
          },
          "feeAmount": {
            "type": "string",
            "nullable": true
          },
          "taker": {
            "type": "string"
          },
          "quoteTtlMs": {
            "type": "integer"
          },
          "replyBy": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "The winning reply's own accept cutoff, not the corridor cap."
          },
          "routing": {
            "type": "object",
            "properties": {
              "preferenceApplied": {
                "type": "boolean"
              },
              "restrictionApplied": {
                "type": "boolean"
              },
              "fallbackUsed": {
                "type": "boolean"
              },
              "preferredQuotesReceived": {
                "type": "integer"
              },
              "openMarketQuotesReceived": {
                "type": "integer"
              }
            }
          },
          "noQuoteReason": {
            "type": "string",
            "nullable": true
          },
          "txHash": {
            "type": "string",
            "nullable": true
          },
          "submittedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "filledAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "failReason": {
            "type": "string",
            "nullable": true
          },
          "lateFillCorrectionClosed": {
            "type": "boolean",
            "description": "True only for FAILED/EXPIRED RFQs after the server's 24-hour late-fill correction window. Clients may retire broadcast recovery once this is true."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateRfqRequest": {
        "type": "object",
        "required": [
          "chainId",
          "sellToken",
          "buyToken"
        ],
        "oneOf": [
          {
            "required": [
              "sellAmount"
            ],
            "not": {
              "required": [
                "buyAmount"
              ]
            }
          },
          {
            "required": [
              "buyAmount"
            ],
            "not": {
              "required": [
                "sellAmount"
              ]
            }
          }
        ],
        "properties": {
          "chainId": {
            "type": "integer"
          },
          "sellToken": {
            "type": "string"
          },
          "buyToken": {
            "type": "string"
          },
          "sellAmount": {
            "type": "string",
            "description": "Exact-input gross spend cap (atomic units). Exactly one of sellAmount / buyAmount. Minimum: 1 whole sell token, or the feeable output plus its injected fee when that is larger."
          },
          "buyAmount": {
            "type": "string",
            "description": "Exact-output amount (atomic units). Exactly one of sellAmount / buyAmount. Minimum: 1 whole buy token. The protocol fee is charged on the sell-token output, so a 1-token buy can still get no_valid_quote if that output is dust."
          },
          "taker": {
            "type": "string",
            "description": "The bound settler. Required on POST /rfq/request. Fills are restricted to this wallet for the whole order life. Preview may omit it."
          },
          "preferredLiquidityWallets": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 10
          },
          "restrictedLiquidityWallets": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 10
          },
          "flowSessionId": {
            "type": "string",
            "description": "Web-only. The Flow session (/v2/flow) this request is the swap leg of. Sent with the session credential in `X-Flow-Access`, it stands in for `takerProof` when `taker` is the session wallet on the session chain — that session was opened with the same signature. A credential that does not vouch falls through to the `takerProof` rules below."
          },
          "takerProof": {
            "type": "object",
            "required": [
              "nonce",
              "issuedAt",
              "signature"
            ],
            "description": "Proof of control: an EIP-712 TakerControl signature by the `taker` wallet over (taker, chainId, nonce, issuedAt), domain { name: \"Textile Taker Control\", version: \"1\", chainId }. Required on POST /rfq/request unless the caller authenticates with a partner API key or an admin session — or holds a grant: a verified proof covers the same caller naming the same taker on the same chain for ~12h, and requests inside that window may omit takerProof. Treat a 401 proof_of_control_required as the cue to sign and resend (it is answered before any maker is solicited). issuedAt must be within 60s of the venue clock, and the nonce is bound to this one signed challenge and to the caller that first presented it (the identical proof may be re-sent by that caller while it is fresh; re-binding the nonce, or the same proof from a different caller, is refused as proof_of_control_replayed). EOA signatures verify by ecrecover; smart-contract takers (Safe, 4337) verify on-chain via EIP-1271, including counterfactual accounts through ERC-6492. Missing or bad proof is a 401 with details.reason proof_of_control_required | proof_of_control_stale | proof_of_control_invalid | proof_of_control_replayed; a chain that cannot answer the EIP-1271 read is a 502 with details.reason taker_control_unverifiable (retry the same proof). Ignored on /rfq/preview.",
            "properties": {
              "nonce": {
                "type": "string",
                "pattern": "^0x[0-9a-fA-F]{64}$",
                "description": "32 random bytes, hex."
              },
              "issuedAt": {
                "type": "integer",
                "description": "Unix milliseconds at signing time."
              },
              "signature": {
                "type": "string"
              }
            }
          }
        }
      },
      "RfqPreviewOutcome": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "preview",
                  "no_quote"
                ]
              },
              "reason": {
                "type": "string",
                "description": "no_quote only: no_restricted_liquidity | no_makers_online | no_valid_quote"
              },
              "sellAmount": {
                "type": "string"
              },
              "buyAmount": {
                "type": "string"
              },
              "feeAmount": {
                "type": "string"
              },
              "takerPays": {
                "type": "string"
              },
              "rateRay": {
                "type": "string"
              },
              "availableSellAmount": {
                "type": "string",
                "description": "Packed published sell-token depth after the min-slice floor."
              },
              "availableBuyAmount": {
                "type": "string"
              },
              "reservedUntil": {
                "type": "string",
                "format": "date-time",
                "description": "no_quote only: a live firm quote is holding the liquidity THIS request would have used, until this time, so the book reads thinner (or empty) than the maker really is. Scoped to the request: a reservation on the opposite direction (it locks the maker’s balance of your buy token, not the other one), or one held by a funding wallet outside a restricted list (inventory is per Permit2 owner, and one maker may register several wallets), is not reported. Absent when nothing relevant is reserved — that no_quote is a genuinely empty book, not a race."
              },
              "retryAfterMs": {
                "type": "integer",
                "description": "no_quote only, alongside reservedUntil: milliseconds to wait before re-previewing. Already includes the slack for the venue to release the reservation and the maker to republish. Retrying sooner re-reads the same reserved book."
              },
              "routing": {
                "type": "object",
                "properties": {
                  "preferenceApplied": {
                    "type": "boolean"
                  },
                  "restrictionApplied": {
                    "type": "boolean"
                  },
                  "fallbackUsed": {
                    "type": "boolean"
                  },
                  "targetMakerWallets": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "preferredQuotesReceived": {
                    "type": "integer"
                  },
                  "openMarketQuotesReceived": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "RfqOutcome": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "rfqId": {
                "type": "string"
              },
              "claimToken": {
                "type": "string",
                "description": "Ownership credential for this RFQ, returned exactly once, here. Send it as the X-Rfq-Claim header on cancel, submit, and status. Without a partner key it is the only thing that authorizes those calls; the rfqId does not, because every solicited maker receives it. Store it with the quote."
              },
              "status": {
                "type": "string",
                "enum": [
                  "quoted",
                  "no_quote"
                ]
              },
              "reason": {
                "type": "string",
                "description": "no_quote only: no_restricted_liquidity | no_makers_online | no_valid_quote"
              },
              "availableSellAmount": {
                "type": "string",
                "description": "no_quote only, when the request named sellAmount: packed published depth on the sell token. Omitted when zero. If this is below the requested size, retry at this amount."
              },
              "availableBuyAmount": {
                "type": "string",
                "description": "no_quote only, when the request named buyAmount: packed published depth on the buy token. Omitted when zero."
              },
              "reservedUntil": {
                "type": "string",
                "format": "date-time",
                "description": "no_quote only: a live firm quote is holding the liquidity THIS request would have used, until this time, so the book reads thinner (or empty) than the maker really is. Scoped to the request: a reservation on the opposite direction (it locks the maker’s balance of your buy token, not the other one), or one held by a funding wallet outside a restricted list (inventory is per Permit2 owner, and one maker may register several wallets), is not reported. Absent when nothing relevant is reserved — that no_quote is a genuinely empty book, not a race."
              },
              "retryAfterMs": {
                "type": "integer",
                "description": "no_quote only, alongside reservedUntil: milliseconds to wait before asking again. Already includes the slack for the venue to release the reservation and the maker to republish. Retrying sooner re-reads the same reserved book."
              },
              "quote": {
                "type": "object",
                "properties": {
                  "sellAmount": {
                    "type": "string"
                  },
                  "buyAmount": {
                    "type": "string"
                  },
                  "feeAmount": {
                    "type": "string"
                  },
                  "takerPays": {
                    "type": "string"
                  },
                  "rateRay": {
                    "type": "string"
                  },
                  "expiresAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "orderDeadline": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "latestOrderDeadline": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Latest signed-order deadline in the bundle — the last instant any slice is still executable. Equals orderDeadline for a single-maker quote."
                  },
                  "reactor": {
                    "type": "string"
                  },
                  "spender": {
                    "type": "string",
                    "description": "Contract the taker must approve and that transactions.swap targets. Equals reactor on an ordinary quote. Differs when the venue routed the fill through the VaultOrderExecutor (a yield-vault order whose settlement may sit in the adapter): approve THIS address, not reactor, or the fill reverts on transferFrom."
                  },
                  "taker": {
                    "type": "string"
                  },
                  "encodedOrder": {
                    "type": "string"
                  },
                  "signature": {
                    "type": "string"
                  },
                  "orders": {
                    "type": "array",
                    "description": "Every signed order in the quote. One maker when a single quote covers the size; several when the venue bundled makers. Always execute transactions.swap, which already batches this list.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "encodedOrder": {
                          "type": "string"
                        },
                        "signature": {
                          "type": "string"
                        },
                        "sellAmount": {
                          "type": "string"
                        },
                        "buyAmount": {
                          "type": "string"
                        },
                        "feeAmount": {
                          "type": "string"
                        },
                        "maker": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "transactions": {
                "type": "object",
                "description": "Ready-to-broadcast calldata for the firm quote. Send `approval` first (skippable when the standing allowance already covers takerPays), then `swap`.",
                "required": [
                  "approval",
                  "swap"
                ],
                "properties": {
                  "approval": {
                    "$ref": "#/components/schemas/UnsignedTransaction"
                  },
                  "swap": {
                    "$ref": "#/components/schemas/UnsignedTransaction"
                  }
                }
              },
              "routing": {
                "type": "object",
                "properties": {
                  "preferenceApplied": {
                    "type": "boolean"
                  },
                  "restrictionApplied": {
                    "type": "boolean"
                  },
                  "fallbackUsed": {
                    "type": "boolean"
                  },
                  "targetMakerWallets": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "preferredQuotesReceived": {
                    "type": "integer"
                  },
                  "openMarketQuotesReceived": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "request_id": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "description": "Structured detail, when the error has a machine-readable form. Branch on this rather than on `message`, which is prose and can be reworded.",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Stable reason code.\n`corridor_unavailable` (400): this pair cannot be quoted over RFQ — either no corridor exists for it on that chain, or the corridor is still in BOOK mode. Route the taker to /v1 or another pair.\n`insufficient_funds` (400): the named taker does not hold enough of the sell token. Carries atomic `required` / `available`, plus `committed` when the taker's other live quotes are what spoke for the balance.\n`proof_of_control_*` (401): the `takerProof` was missing, stale, from the wrong wallet, or replayed. Sign a fresh challenge.\n`taker_control_unverifiable` (502): the EIP-1271 read for a smart-account taker failed. The proof is still good — retry with the same one.",
                    "enum": [
                      "corridor_unavailable",
                      "insufficient_funds",
                      "proof_of_control_required",
                      "proof_of_control_stale",
                      "proof_of_control_invalid",
                      "proof_of_control_replayed",
                      "taker_control_unverifiable"
                    ]
                  },
                  "required": {
                    "type": "string",
                    "description": "`insufficient_funds` only: atomic sell-token units the request needs."
                  },
                  "available": {
                    "type": "string",
                    "description": "`insufficient_funds` only: the taker's raw sell-token balance, atomic."
                  },
                  "committed": {
                    "type": "string",
                    "description": "`insufficient_funds` only, and only when the wallet could fund this request alone: atomic sell-token units already reserved by the taker’s other live quotes. Wait for one to expire, or execute it."
                  }
                }
              }
            }
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Error envelope",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
