Integration Reference

OTC Orders and RFQ

Integrate EVM PT/YT rate orders, signed quote requests and MetaVault responses.

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. Its canonical fee-order profile and API are separate from the OTC/RFQ flow below.

This is the EVM OTC integration. Stellar limit orders use a different engine and API. OTC is currently enabled on Base (8453), Katana (747474) and Flare (14).

Resolve the deployment

See the deployed contracts for Base, Katana and 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 fieldRegistry key
limitOrderProtocolspectra.otc.settlement
rateAmountGetterspectra.otc.rate_getter
authorizedTakersGuardspectra.otc.takers_guard
orderSignerspectra.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

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.

TermMeaning
derivativeKind0 = PT, 1 = YT.
quoteKind0 = underlying, 1 = IBT, from the same market.
makingAmountFixed total quantity sent by the maker, in that token's base units.
takingAmountCreation-time baseline; use the getter for the current payment.
impliedApyWadAnnually compounded rate: 5% is 50000000000000000.
ExpiryDeadline 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):

PolicyTraitsBehavior
Partial fills (the apps' default)ALLOW_MULTIPLE_FILLS set, NO_PARTIAL_FILLS clearAny number of fills, each at most what is left. The settlement tracks the remainder by order hash.
No partial fillsNO_PARTIAL_FILLS set, ALLOW_MULTIPLE_FILLS clearOne 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.

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.

ERC-4626 compliance required.

Spectra assumes its interest-bearing tokens (IBTs) conform to the ERC-4626 standard. Deviations from its exchange-rate, conversion and preview behavior can cause incorrect pricing, accounting discrepancies or failed protocol operations.

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:

MakerEntrypoint
EOAfillOrderArgs, using the compact EIP-2098 signature.
Contract walletfillContractOrderArgs, 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

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

Method and pathRequest
GET /ordersOptional market (PT address), makerAddress, derivativeKind, includeInactive, limit, offset.
POST /create{ envelope: OtcOrderEnvelope }.
GET /rfq/requestsExactly 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

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

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.

On this page