Response payloads
The aggregator candidate (agg)
The agg object is the aggregator's own quote, reproduced 1:1. It is the input to /v1/calldata, and its structure is a route description rather than a transaction.
Quote fields
| Field | Type | Meaning |
|---|---|---|
srcToken | string | Token sold; zero address for native |
dstToken | string | Token bought; zero address for native |
chainId | number | Chain this route is valid on, and the chain the encode will target |
tradeType | string | Always exactIn for an encodable quote |
inputAmount | string | Total sold, smallest unit of srcToken |
outputAmount | string | Total bought before slippage, smallest unit of dstToken |
gasUseEstimate | string | Estimated gas units for the whole route |
gasUseEstimateUsd | string | Estimated gas cost in USD, scaled by 1e18 |
priceImpactBps | number | Price impact across the route, basis points |
aggregatorAddress | string | Router contract the encoded call targets |
routes | array | Split legs, see below |
swapFeeBps | number | Swap fee charged on this trade, in basis points; echoes the request parameter. Defaults to 0 if no swapFeeBps was requested |
swapFee | string | Swap fee amount for this trade, in the same units as outputAmount. All amounts are rounded down; an amount below 1 unit rounds down to 0. Defaults to 0 if no swapFeeBps was requested |
gasUseEstimateUsd is fixed-point: "9493065552309952" is 0.009493 USD. Divide by 1e18 before display.
outputAmount is the quoted amount before slippage. The minimum the transaction will accept is derived from it and slippageTolerance during encoding, and is not surfaced as a separate field.
Routes and paths
A quote splits the input across one or more routes, each of which is a sequence of hops.
| Field | Type | Meaning |
|---|---|---|
percent | number | Share of the input on this route, 0 to 100 |
inputAmount | string | This route's share, in absolute smallest units |
outputAmount | string | This route's contribution to the total output |
priceImpactBps | number | Impact on this route alone |
path | array | Tokens traversed, in order, including the endpoints |
pools | array | Pools traversed; one entry per hop, so path.length - 1 entries on a pool-based route |
The inputAmount values across routes sum to the quote's inputAmount, and /v1/calldata enforces that invariant. percent is a rounded convenience figure; do arithmetic on the absolute amounts instead.
Each path entry carries address, decimals, and symbol. decimals is the only reliable way to render the intermediate amounts of a multi-hop route; symbol is informational and is not guaranteed unique.
A route with an empty pools array is legitimate: it describes a wrap or unwrap between a native asset and its wrapped form, which involves no pool. Such a payload is encoded unchanged.
Pools
| Field | Presence | Meaning |
|---|---|---|
address | always | Pool identity: a 40-hex contract address, or a 64-hex pool ID for V4-style pools |
type | always | Pool family, e.g. v3, univ3, univ4, propamm_pcs, stableswap |
fee | always | Static LP fee in pips; for dynamic-fee Infinity pools this is the dynamic-fee flag 8388608 (0x800000) rather than a rate |
resolvedFee | usually | The fee actually charged on this hop, in pips |
poolManager | V4-style pools | Contract that owns the pool |
provider | always | Liquidity source, e.g. pancakeswap, uniswap, aerodrome |
token0, token1 | always | Pool's token pair, not necessarily in swap direction |
inputAmount | RFQ pools | Amount feeding this specific hop |
hooks | always, nullable | Hook contract for V4-style pools; null when none |
parameters | always, nullable | Packed pool parameters; null when not applicable |
tickSpacing | CL pools | Tick spacing for concentrated-liquidity pools |
address is polymorphic by length. A 40-hex value is a deployed pool contract; a 64-hex value is a V4-style pool ID that identifies a pool inside a poolManager. Code that assumes 20-byte addresses will reject valid quotes on chains where Infinity or Uniswap V4 liquidity is routed through.
Read resolvedFee rather than fee when displaying the fee a swap actually pays. The two agree for V2, V3, and Stable pools; they diverge for dynamic-fee pools, where fee is a flag and resolvedFee is the rate the hook resolved at quote time.
hooks and parameters are emitted as explicit null when inapplicable, unlike most optional fields in this API, because they are passed through from the engine unchanged.
Aggregator-origin error codes
When the aggregator refuses a request, the gateway returns 502 UPSTREAM_ERROR and carries the engine's numeric code through. On /v1/calldata it arrives as upstream.code; on /v1/quote it appears inside message, for the reason given in Upstream detail.
upstream.code | Meaning |
|---|---|
40100 | Chain not supported by the encoder |
40101 | Invalid input |
40102 | Invalid hex input |
40104 | Invalid pool data in the request |
40401 | Not found |
42102 | Quote unavailable, no route could be produced |
42103 | Invalid API key |
42104 | Encoding failed |
50001 | Internal error |
50301, 50302, 50303 | Temporarily unavailable |
42102 reports that no route exists for that pair and size at that moment, which is a market condition rather than a defect, and it is the code an integrator sees most often. Surface it to the user as "no route". A PancakeSwap X quote can still succeed on the same pair, so requesting agg,pcsx or auto rather than agg turns that failure into a single-candidate success.
The PancakeSwap X candidate (pcsx)
The pcsx object is a complete, signable Dutch-decay limit order together with the EIP-712 payload needed to sign it. Reproduce it 1:1.
Top-level fields
| Field | Type | Presence | Meaning |
|---|---|---|---|
quoteId | string | always | The engine's own quote identifier; not the gateway's usq_ id |
requestId | string | always | Correlation identifier for support requests |
rfqVersion | v1, v3 or v4 | always | Order family; see Order families |
slippageTolerance | string | always | Percent, as a string: "0.5" is 0.5% |
startTimeBufferSecs | number | always | Delay from quote time to the start of the decay window |
auctionPeriodSecs | number | always | Length of the decay window |
deadlineBufferSecs | number | always | Grace after the window before the order dies |
permitData | object | always | EIP-712 payload to sign |
orderInfo | object | always | Decoded order body |
encodedOrder | string | always | ABI-encoded order bytes; what you submit |
orderHash | string | always | Hash of the order; the handle for /v1/status |
recipient | string | when set | Output recipient when it differs from the swapper |
portionBips | number | when a fee applies | Fee share in basis points |
portionAmount | string | when a fee applies | Fee amount in output units |
portionRecipient | string | when a fee applies | Fee destination |
slippageTolerance here is a percent string, while the unified candidate's slippageTolerance is a fraction number. "0.5" in pcsx and 0.005 on the candidate describe the same tolerance. The gateway performs that conversion for you on the way in; the two representations coexist in the response because each is verbatim for its own layer.
orderInfo
| Field | Type | Presence | Meaning |
|---|---|---|---|
reactor | string | always | Settlement contract that will execute the order |
swapper | string | always | Address that signs and owns the order; equals the recipient you requested |
nonce | string | always | 256-bit nonce, decimal |
deadline | string | always | Unix seconds; the order is unfillable after this |
additionalValidationContract | string | always | Zero address when no extra validation applies |
additionalValidationData | string | always | "0x" when none |
decayStartTime | string | v1, advisory on v3/v4 | Start of the decay window, unix seconds |
decayEndTime | string | v1, advisory on v3/v4 | End of the decay window, unix seconds |
exclusiveFiller | string | usually | Filler with priority during the exclusivity window |
exclusivityOverrideBps | string | usually | Premium another filler must beat to override exclusivity; "100" is 1% |
cosigner | string | v3, v4 | Address that cosigns the order at submission |
input | object | always | { token, startAmount, endAmount } |
outputs | array | always | { token, startAmount, endAmount, recipient, isPortion? } |
Timing
The three buffer fields and the decay anchors are related arithmetically, with the quote's own creation time as the origin:
decayStartTime = quotedAt + startTimeBufferSecs
decayEndTime = decayStartTime + auctionPeriodSecs
deadline = decayEndTime + deadlineBufferSecsTypical values are a 60-second start buffer, a 120-second auction, and a 12-second deadline buffer, giving an order that becomes fillable a minute after quoting and dies about three minutes after that. Those are defaults, not guarantees so you should still read the actual numbers from the payload.
The start buffer is why a submitted order sits at OPEN unfilled for roughly the first minute: no filler can act before decayStartTime.
Decay and amounts
The order is a Dutch auction over the output. outputs[i].startAmount is the amount the swapper would receive at decayStartTime, endAmount the amount at decayEndTime, and startAmount >= endAmount always holds. Between those points the acceptable amount decays linearly, so a filler that acts early pays more. The swapper is guaranteed no worse than endAmount, which is where the signed slippage tolerance is expressed.
For an exactIn order the input is fixed: input.startAmount equals input.endAmount. For exactOut the input side carries the decay instead.
The gateway's outputAmount on the candidate is the sum of startAmount across non-portion outputs, which makes it comparable to the aggregator's pre-slippage outputAmount. When comparing worst cases across engines, compare endAmount against the aggregator's slippage-adjusted minimum instead.
permitData and signing
permitData is a complete EIP-712 payload:
{
"domain": {
"name": "Permit2",
"chainId": 1,
"verifyingContract": "0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768"
},
"types": { "PermitWitnessTransferFrom": [ "…" ], "TokenPermissions": [ "…" ], "ExclusiveDutchOrder": [ "…" ], "OrderInfo": [ "…" ], "DutchOutput": [ "…" ] },
"values": { "permitted": { "…": "…" }, "spender": "0x…", "nonce": "…", "deadline": "…", "witness": { "…": "…" } },
"primaryType": "PermitWitnessTransferFrom"
}values is the message. The field is named values, but EIP-712 signing libraries expect the struct under the key message. Pass permitData.values as the message; do not pass the permitData object as a whole. Getting this wrong produces a valid signature over the wrong struct, which the engine will reject at submission.
Read verifyingContract from the response. The Permit2 contract is not at the same address on every deployment, because some reactor families verify against their own Permit2 instance. Use the value in permitData.domain, never a hardcoded constant.
The domain has no version. Permit2's EIP-712 domain is name, chainId, and verifyingContract only. Passing version: undefined is harmless in libraries that drop undefined keys and fatal in those that do not therefore you should construct the domain from exactly the keys present.
Before signing, the swapper/recipient must have approved Permit2 (permitData.domain.verifyingContract) to spend the input token. This is a one-time approval per token per Permit2 instance, and it is separate from the per-order signature. An order signed without it will be submitted successfully and then fail its on-chain pre-flight.
The signature is a 65-byte secp256k1 signature over the typed data. Send it as signature alongside the unmodified candidate.
Portion outputs
When a fee applies, the order carries an additional output marked isPortion: true, and the top-level portionBips, portionAmount, and portionRecipient describe it. That output is part of what the swapper signs and part of what settles on chain, and it is excluded from the candidate's outputAmount so that cross-engine comparison reflects what reaches the recipient. When rendering the order, show the portion output separately rather than summing all outputs.
Order families
rfqVersion reports which family was built. v1 orders are time-anchored: the decay window in orderInfo is authoritative and is what the swapper signs. v3 and v4 orders are cosigned: a cosigner address is present, and the authoritative decay anchors are set at submission time by the settlement layer, so the decayStartTime and decayEndTime in the quote are advisory hints. This is visible downstream: the status response for a cosigned order omits decayStartTime and decayEndTime entirely.
For an integrator this changes little: display the advisory window as approximate on cosigned orders, and expect status responses without decay anchors. Signing, submission, and polling are identical across families.
The PancakeSwap X SDK
The SDK (@pancakeswap/pcsx-sdk) is the reference implementation of the order format. Integrating through the Unified Swap API does not require it: permitData gives you everything needed to sign, encodedOrder everything needed to submit, and orderHash everything needed to track.
The SDK is useful when you want to verify the payload instead of trusting it. ExclusiveDutchOrder.parse(encodedOrder, chainId) (and the ExclusiveDutchOrderV3 / ExclusiveDutchOrderV4 equivalents for cosigned families) decodes the bytes you are about to sign back into a structured order. From there, .hash() recomputes the order hash and .permitData() recomputes the typed-data payload. Checking both against the values in the response confirms that the payload you received is internally consistent and that encodedOrder really encodes the order the response describes.
The choice of parser class follows rfqVersion: v1 to ExclusiveDutchOrder, v3 to ExclusiveDutchOrderV3, v4 to ExclusiveDutchOrderV4. Parsing cosigned bytes with the V1 class fails with a misleading error, so branch on the field rather than trying each in turn.
For clients that hold nothing but an order hash, /v1/status is sufficient and the SDK adds little.