# OTC Orders and RFQ



Spectra OTC settles individual PT/YT trades through Spectra's deployment of
the 1inch Limit Order Protocol. The API validates and stores signed orders;
it does not hold funds or execute fills. RFQ adds a non-binding request and
response workflow around those same orders.

For the public order book, partial fills and AMM/book batch execution, use the
[EVM limit-order reference](/docs/integration-reference/evm-limit-orders).
Its canonical fee-order profile and API are separate from the OTC/RFQ flow below.

This is the **EVM OTC** integration. [Stellar limit orders](/docs/stellar/router-and-limit-orders)
use a different engine and API. OTC is currently enabled on &#x2A;*Base (8453)**,
&#x2A;*Katana (747474)** and &#x2A;*Flare (14)**.

## Resolve the deployment [#resolve-the-deployment]

See the deployed contracts for [Base](/docs/technical-reference/deployed-contracts#base),
[Katana](/docs/technical-reference/deployed-contracts#katana) and
[Flare](/docs/technical-reference/deployed-contracts#flare).

Resolve current addresses at runtime with
`GET https://api.spectra.finance/v1/{network}/otc/config`, where
`network` is a supported chain ID or network slug. A successful response is
`{ success: true, data: { limitOrderProtocol, rateAmountGetter,
authorizedTakersGuard, orderSigner } }`.

These addresses come from the execution chain's global MetaVaultsRegistryV2
keys:

| Response field          | Registry key               |
| ----------------------- | -------------------------- |
| `limitOrderProtocol`    | `spectra.otc.settlement`   |
| `rateAmountGetter`      | `spectra.otc.rate_getter`  |
| `authorizedTakersGuard` | `spectra.otc.takers_guard` |
| `orderSigner`           | `spectra.otc.order_signer` |

The endpoint is uncached. A `503` can mean that OTC is disabled or a complete
deployment cannot be resolved. Treat that as unavailable; do not substitute
test addresses. Keep each stored order's original settlement for history
and cancellation when the current configuration changes.

## Rate-order model [#rate-order-model]

The maker signs an eight-field 1inch v4 order with Spectra's getter installed
on both amount-getter legs. The extension encodes the PT address, derivative
kind, quote kind and implied APY. The salt binds the extension to the order.

| Term             | Meaning                                                             |
| ---------------- | ------------------------------------------------------------------- |
| `derivativeKind` | `0` = PT, `1` = YT.                                                 |
| `quoteKind`      | `0` = underlying, `1` = IBT, from the same market.                  |
| `makingAmount`   | Fixed total quantity sent by the maker, in that token's base units. |
| `takingAmount`   | Creation-time baseline; use the getter for the current payment.     |
| `impliedApyWad`  | Annually compounded rate: 5% is `50000000000000000`.                |
| Expiry           | Deadline for filling, distinct from the PT/YT maturity.             |

The getter recalculates the counter-amount at execution. Do not price a fill
from the stored baseline or floating-point display values. Use each token's
decimals. The supported pairs are PT ↔ underlying, PT ↔ IBT, YT ↔ underlying
and YT ↔ IBT. No AMM pool is required.

An order signs one of two fill policies in `makerTraits`, and nothing else is
accepted (`isSignableOtcMakerTraits`):

| Policy                            | Traits                                               | Behavior                                                                                           |
| --------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Partial fills (the apps' default) | `ALLOW_MULTIPLE_FILLS` set, `NO_PARTIAL_FILLS` clear | Any number of fills, each at most what is left. The settlement tracks the remainder by order hash. |
| No partial fills                  | `NO_PARTIAL_FILLS` set, `ALLOW_MULTIPLE_FILLS` clear | One fill of the whole `makingAmount`, closed through the maker's nonce bit.                        |

Both flags set, or neither, is rejected. A partial order still carries a
nonce, which the API keeps unique per maker. Orders also require
`receiver = 0` and `allowedSender = 0`. Private orders use
`SpectraAuthorizedTakersGuard` with up to 64 authorized addresses. It checks
the immediate caller of the fill: for a MetaVault taker, authorize the Safe.
The list is signed data, not confidential data.

<Callout type="warn">
  **Private OTC orders: use direct settlement.**

  Do not authorize the BatchFiller as a taker for private OTC orders or use it
  to fill them. Fill private OTC orders directly through `LimitOrderProtocol`.
</Callout>

<Callout type="warn">
  **ERC-4626 compliance required.**

  Spectra assumes its interest-bearing tokens (IBTs) conform to the
  [ERC-4626 standard](https://eips.ethereum.org/EIPS/eip-4626). Deviations from
  its exchange-rate, conversion and preview behavior can cause incorrect
  pricing, accounting discrepancies or failed protocol operations.
</Callout>

## Publish and fill [#publish-and-fill]

Use the primitives in `@perspective/spectra-sdk/otc/codec`, `otc/types`,
`otc/validation` and `otc/abi`.

1. Resolve configuration and verify the market's PT, YT, IBT and underlying
   addresses onchain.
2. Build the canonical extension, salt and traits, choosing the fill policy
   with `buildMakerTraits({ ..., allowPartialFills })`. Allocate a unique
   nonce per maker and settlement domain; reusing it can invalidate multiple
   orders.
3. Call the getter's `previewOrder`, set the creation baseline and cross-check
   the local hash with the settlement's `hashOrder`.
4. Approve the maker asset to the settlement and sign the order. Its EIP-712
   domain is `1inch Limit Order Protocol`, version `4`, with the chain ID and
   settlement as `verifyingContract`.
5. POST `{ envelope }` to `/v1/{network}/otc/create`.

The `OtcOrderEnvelope` uses `schema: "spectra-otc"`, `schemaVersion: 1` and
`orderType: "RATE"`. It carries `chainId`, `settlementAddress`, `orderHash`,
`order`, `extension`, `signature`, `createdAt`, `createdAtBlock` and, for
private orders, `authorizedTakers`. Serialize uints as decimal strings.
`metadata` is untrusted display assistance.

`validateOtcEnvelope(envelope, chainId, instance)` checks signed structure,
configuration, traits, expiry, hash and extension binding. It is a pure check:
it does **not** verify signatures, balances, allowances or current fillability.
Refresh those from the chain, including the bit invalidator and a fresh getter
preview. Failed reads must remain unknown.

Read what an order has left by its policy:

* **Partial fills:** `rawRemainingInvalidatorForOrder(maker, orderHash)`
  returns `0` while untouched (the whole `makingAmount` is left), otherwise
  `~remaining`; `~0` (nothing left) once filled or cancelled.
  `otcRemainingFromRaw(raw, makingAmount)` decodes it.
* **No partial fills:** pass the **full nonce** to
  `bitInvalidatorForOrder(maker, nonce)`; the contract shifts it internally.
  Test bit `nonce & 255` in the returned bitmap.

A closed order takes precedence over low balance or allowance when
displaying it. Compare the maker's balance and allowance with what is left,
not with the whole `makingAmount`.

For a fill, require an explicit maximum payment, approve the payment token,
then simulate in maker-amount mode with the extension and threshold packed by
`buildTakerTraits`. The `amount` argument is the making amount to take: the
whole `makingAmount` without partial fills, at most what is left with them
(`otcFillMakingAmount` sizes it from a PT/YT quantity). A part is priced pro
rata of the whole order's live price: `otcTakingAmountFor(preview.takingAmount,
amount, makingAmount)`, rounded up; add your buffer to set the threshold.
The settlement fills at most what is left, so read the remainder right before
filling: a competing fill can otherwise make yours smaller than intended. Choose the entrypoint by the **maker's deployed code**:

| Maker           | Entrypoint                                                              |
| --------------- | ----------------------------------------------------------------------- |
| EOA             | `fillOrderArgs`, using the compact EIP-2098 signature.                  |
| Contract wallet | `fillContractOrderArgs`, passing the original ERC-1271 signature bytes. |

A 65-byte signature does not prove that the maker is an EOA. Contract signature
validity can change, so verify it again before execution. For non-ECDSA contract
signatures, including a Safe's empty `0x`, call `isValidSignature(orderHash,
signature)` directly and require `0x1626ba7e`; reject failed or reverting reads.
A successful preview or simulation does not reserve the order.

Cancel with `cancelOrder(makerTraits, orderHash)` from the maker on the order's
original settlement; on a partially filled order it withdraws what is left.
Delisting, API shutdown and allowance revocation do not invalidate the
signature. A set bit, or nothing left, only says **filled or cancelled**:
every fill emits `OrderFilled(orderHash, remainingAmount)`, a cancel does not.
`resolveOtcOrderClose` (`otc/outcome`) reads which, and what a cancel withdrew.

## API reference [#api-reference]

All paths below follow `/v1/{network}/otc`. Successful list calls return
`{ success: true, data }`; order creation returns `{ success: true, orderHash }`.

| Method and path     | Request                                                                                                 |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| `GET /orders`       | Optional `market` (PT address), `makerAddress`, `derivativeKind`, `includeInactive`, `limit`, `offset`. |
| `POST /create`      | `{ envelope: OtcOrderEnvelope }`.                                                                       |
| `GET /rfq/requests` | Exactly one of `requester` or `vault`; requester history can use `includeClosed=true`.                  |
| `POST /rfq/create`  | `{ envelope: QuoteRequestEnvelope }`.                                                                   |
| `POST /rfq/respond` | `{ requestHash, vault, orderHash }` for an already published response.                                  |
| `POST /rfq/decline` | `{ requestHash, vault, signer, signature }`.                                                            |
| `POST /rfq/cancel`  | `{ requestHash, signature }`.                                                                           |

Each listed envelope's `metadata` restates, for display, the last
revalidation: `invalidated`, `outcome` (`FILLED` or `CANCELLED`, once read),
`unfillableReason`, `allowsPartialFills` and `remainingMakingAmount` (what the
order has left; once closed, what it left unfilled). A partially filled order
stays listed while some is left. Signed terms must still come from the order
and extension, and fillability from the chain.

Load maker history separately with `makerAddress`, `includeInactive=true`
and pagination. A page of the public book is not a complete list of a maker's
outstanding signatures. Validate historical identity with
`validateOtcOrderIdentity`; do not require old orders to match today's deployment
just to show or cancel them.

Write endpoints distinguish malformed input (`400`), semantic validation
failure (`422`) and unavailable service (`503`). An identical accepted order
can be published again without creating a new order; this does not reactivate
an expired or invalidated signature.

## RFQ lifecycle [#rfq-lifecycle]

Use `@perspective/spectra-sdk/otc/rfq` for the canonical types, typed-data
definitions, hashing and wire conversion. The `Spectra OTC RFQ` EIP-712 domain
has version `1`, a chain ID and **no verifying contract**: these signatures
authenticate API operations only.

1. The requester signs `QuoteRequest`: requester, derivative/quote kinds,
   requester-side `BUY` or `SELL`, underlying, IBT, PT, maturity, derivative
   amount, recipient Safe addresses, request expiry and salt.
2. POST its `QuoteRequestEnvelope`. The API allows up to 16 recipient vaults
   and 20 open requests per requester per chain. The expiry cannot exceed maturity.
3. Each vault either signs `DeclineQuoteRequest` with an authorized operator,
   or publishes a private order on the opposite side and links it with
   `/rfq/respond`. Linking requires no extra signature: the stored vault-signed
   order proves the offer's authorization.
4. The requester view includes per-vault `PENDING`, `DECLINED` or `RESPONDED`
   states and response order envelopes. Validate and fill a response through
   the normal order flow.

The response must match the market and derivative/quote kinds and authorize
the requester. Linking does not enforce that its live quantity equals the
requested amount; display the actual offer terms before acceptance.

For a new market, set request `pt` to the zero address. The responding curator
deploys a PT for the requested IBT. The API accepts maturity from the requested
timestamp through 24 hours later to accommodate the factory's relative-duration
deployment. Show the resulting maturity to the requester.

`CancelQuoteRequest` withdraws the request. Cancellation, expiry or filling
another response does not invalidate any response order. A declined recipient
cannot become responded; an existing response can only be relinked to the
same order.

## Safe makers [#safe-makers]

Use `buildOtcSignOrderIntent` from `otc/signing` to validate the envelope and
build a structured `signOrder(Order)` delegatecall to the configured signer.
Plan approval and signing separately; a queued proposal is not a valid signature.
Publish only after confirmed execution and live ERC-1271 verification.

The per-market `curator-otc-order` template constrains the Safe maker, receiver,
asset pairs, traits and signer target. It grants signing once per fill policy.
A vault that applied the template before partial fills existed can sign orders
without partial fills only, until the template is applied again. The signer does not inspect the extension;
shared validation remains necessary. Generic message-signing permission is not
required. Generated approval amounts are not permission-level spending caps.

The current curator interface fills EOA-made orders only. Safe-made offers
can be taken through the retail app's contract-maker path. A Safe-made partial
order checks its ERC-1271 signature on the first fill only: cancel it to stop
further fills. Batch fills and
tokenization during a fill are outside this OTC flow.
