Skip to content

PancakeSwap X: Market Maker Integration

PCSX uses an RFQ (request-for-quote) flow for market makers to provide liquidity. PancakeSwap calls your endpoint on demand and you respond with a quote or a decline — no persistent connection is required.

Contracts and sample executors: pancake-x-contracts on GitHub.

RFQ integration

The RFQ flow is a stateless HTTP integration and has three phases:

  1. Indicative quote — PancakeSwap broadcasts quote requests to all MMs in parallel (~500 ms deadline).
  2. Firm fill — When a user accepts a quote, PancakeSwap notifies the winning MM with signed order details; that MM has an exclusivity window to fill.
  3. Dutch auction — Once the exclusivity window expires, the order becomes open to all MMs at a decaying price.

Onboarding

Contact the PancakeSwap X team to register your MM. You'll need to provide:

FieldDescription
httpUrlYour JSON-RPC 2.0 endpoint URL (e.g. https://api.yourmm.com/rfq, any route)
httpApiKeyOptional shared secret for request authentication, set via the x-mm-secret header
Source IP (PCS RFQ requester)If you firewall / IP-allowlist inbound requests, allowlist the requester IPs listed below
address (per chain)Your on-chain filler wallet address
supportedTokens (per chain)Token addresses you're willing to quote

RFQ requester source IPs

PancakeSwap calls your RFQ endpoint from the addresses below. If you IP-allowlist inbound traffic, allow every address for the environment you are integrating against — requests arrive from any of them.

EnvironmentRequester IPs
Test137.184.249.154
Production54.249.247.169, 16.76.48.16, 35.73.155.229

Implement a JSON-RPC 2.0 endpoint

Your endpoint must speak JSON-RPC 2.0 and handle three methods. You can host this at any route — the URL is whatever you register as httpUrl during onboarding.

rfq_requestQuote — quote request

PancakeSwap sends this when a user requests a price. You have 500 ms to respond. No response is treated as a decline — respond explicitly to keep latency low for the end user.

Incoming request:
{
  "jsonrpc": "2.0",
  "id": "uuid",
  "method": "rfq_requestQuote",
  "params": [{
    "tokenInChainId": 42161,
    "tokenIn": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
    "tokenOut": "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9",
    "tokenInAmount": "1000000",
    "swapper": "0x...",
    "requestId": "uuid"
  }]
}

Request params (params[0]):

FieldTypeRequiredDescription
tokenInChainIdnumber✅Chain of the input token (e.g. 1, 56, 42161). Cross-chain not yet supported.
tokenInstring✅Address of the token being sold.
tokenOutstring✅Address of the token being bought.
tokenInAmountstring (wei)conditionalSet for EXACT_INPUT. Mutually exclusive with tokenOutAmount — exactly one must be present.
tokenOutAmountstring (wei)conditionalSet for EXACT_OUTPUT. Mutually exclusive with tokenInAmount — exactly one must be present.
swapperstring❌Swapper wallet address. Schema-optional; may be absent.
requestIdstring✅Correlates the request with the response (non-empty).

A request that sets both tokenInAmount and tokenOutAmount, or neither, is rejected before it reaches the MM.

Response — accept:
{
  "jsonrpc": "2.0",
  "id": "uuid",
  "result": {
    "quoteId": "your-unique-quote-id",
    "tokenInAmount": "1000000",
    "tokenOutAmount": "998500",
    "expiry": 1743000000
  }
}
Response — decline:

Return a decline when you are unwilling or unable to fill. Common reasons: unsupported token pair, amount out of range, insufficient inventory, risk limits reached, token/counterparty restrictions.

{
  "jsonrpc": "2.0",
  "id": "uuid",
  "result": {
    "declined": true,
    "reason": "unsupported pair"
  }
}

rfq_quoteAccepted — firm fill notification

Fired when a user signs and submits the order. You must execute the fill on-chain.

Incoming request:
{
  "jsonrpc": "2.0",
  "id": "uuid",
  "method": "rfq_quoteAccepted",
  "params": [{
    "quoteId": "your-unique-quote-id",
    "orderHash": "0x...",
    "encodedOrder": "0x...",
    "signature": "0x...",
    "chainId": 42161,
    "offerer": "0x...",
    "rfqVersion": "v4"
  }]
}

For V3 and V4 orders, encodedOrder is the full post-cosigned V3DutchOrder / V4DutchOrder ABI encoding (cosignature embedded); signature remains the user's Permit2 signature. The rfqVersion field (v1 / v3 / v4) states the order version directly — v1 = ExclusiveDutch (reactorType 0), v3 = V3Dutch (1), v4 = V4Dutch (2).

Respond with accepted: true to acknowledge receipt. The fill itself happens asynchronously. If you miss the exclusivity window, the order decays and becomes open to all MMs.

{
  "jsonrpc": "2.0",
  "id": "uuid",
  "result": { "accepted": true }
}

rfq_orderOpen — Dutch auction notification

Fired at order creation to all registered MMs except the exclusive filler, for the tokens and chain of the order. Gives non-exclusive MMs advance notice to prepare.

Incoming request:
{
  "jsonrpc": "2.0",
  "id": "uuid",
  "method": "rfq_orderOpen",
  "params": [{
    "orderHash": "0x...",
    "encodedOrder": "0x...",
    "signature": "0x...",
    "chainId": 42161,
    "rfqVersion": "v4",
    "offerer": "0x...",
    "exclusiveFiller": "0x...",
    "decayStartTime": 1743000000
  }]
}

Request params (params[0]):

FieldTypeRequiredDescription
orderHashstring✅Order hash (non-empty).
encodedOrderstring✅ABI-encoded order bytes. V3 / V4: full V3DutchOrder / V4DutchOrder encoding incl. cosignerData + cosignature.
signaturestring✅User's Permit2 signature.
chainIdnumber✅Chain ID (positive integer).
rfqVersionstring✅Order version: v1 (ExclusiveDutch), v3 (V3Dutch), or v4 (V4Dutch) — maps to reactorType 0 / 1 / 2.
offererstring✅Swapper address.
exclusiveFillerstring✅Filler with exclusive rights until the window ends.
decayStartTimenumberconditionalV1 only — unix seconds when exclusivity ends.
decayStartBlocknumberconditionalV3 / V4 — block number when exclusivity ends.

decayStartTime (v1, unix seconds) or decayStartBlock (v3 and v4, block number) tells you when the order becomes open. The rfqVersion field states the version directly — v3 and v4 are both block-based and structurally similar, so read rfqVersion (or decode the order) rather than inferring the type from field presence or the reactor address.

Respond with received: true. Do not attempt to fill before decayStartTime — the on-chain reactor will reject the transaction or apply an override surcharge.

{
  "jsonrpc": "2.0",
  "id": "uuid",
  "result": { "received": true }
}

Two strategies for handling an open order

Option 1 — Wait for the Dutch auction (normal path)

After decayStartTime, the output amount walks down from the exclusive filler's quoted price toward the AMM floor over the period decayStartTime → decayEndTime. Decide when to fill based on inventory and edge:

  • Fill early in the decay window → better output price for the user, but you're racing every other non-exclusive MM watching the same order.
  • Wait further into the decay → less competition, worse price (closer to the AMM floor), higher chance of winning the fill but thinner margin.

You can model this exactly like a Dutch auction: pick the timestamp where your expected cost-to-fill < decayed-output-amount, and submit then.

Option 2 — Snipe before decayStartTime by paying the override

You're not blocked from filling during the exclusivity window — the order encodes an exclusivityOverrideBps surcharge that's added to the required output if a non-exclusive filler executes early. If you can absorb the surcharge and still be profitable, you can take the fill from the exclusive MM before they execute.

Useful when:

  • The exclusive filler is slow or offline and you spot the order first.
  • The market has moved in your favour since the quote was issued, and the override bps is less than the move.

Compute requiredOutput = baseOutput * (1 + exclusivityOverrideBps / 10_000) before submitting and make sure your fill still clears that bar.

Key constraints

ParameterValue
Quote timeout500 ms
Quote expiry (recommended)~30 s from time of quote
Auth header (if configured)x-mm-secret: <api-key>
Order typeExclusiveDutchOrder / V3DutchOrder / V4DutchOrder (EIP-712 signed)
Fill exclusivityOnly your registered address can fill your quoted orders during the exclusivity window
Exclusivity windowEnds at decayStartTime encoded in the order
Override surchargeexclusivityOverrideBps applied to non-exclusive fillers before decayStartTime
Dutch auction perioddecayStartTime → decayEndTime — output decays to the AMM floor price
Open fillAny registered MM may fill after decayStartTime via rfq_orderOpen or polling GET /orders
Connection keep-aliveTo reduce request latency the service attempts to keep the connection alive. Default 55 s, overridable with the keep-alive response header

Fill via the MultiReactor Router

On chains where the Router is live, orders you receive carry the Router address as their reactor, and one Router per family fills every order type. Filling is a three-step flow.

1. Resolve router + type

Decode the encodedOrder (delivered in rfq_quoteAccepted / rfq_orderOpen) to get both the Router address (→ which family / Permit2) and the order type (→ which function to call). A decode util is available in @pancakeswap/[email protected]:

const { reactor, type } = decodeOrder(encodedOrder)
// reactor -> the Router address (BN / PCS / Robinhood family)
// type    -> "exclusiveDutch" | "v3Dutch" | "v4Dutch"

Fail closed: if the type is unknown, do not fill.

2. Approve once per family

Approve your output tokens to the Router address once — that single approval covers all order types on that Router (previously one approval per reactor).

3. Fill by type (preferred)

Call the by-type entry point with the order's reactorType. The selector is identical for every order type (executeByType = 0xe65a593b, executeByTypeWithCallback = 0x2c089ef5), so an executor that pins a selector reaches future order types with no redeploy.

reactorTypeOrder typeBy-type (preferred)Type-prefixed (alternative)
0ExclusiveDutchexecuteByType(0, order)executeExclusiveDutch
1V3DutchexecuteByType(1, order)executeV3Dutch
2V4DutchexecuteByType(2, order)executeV4Dutch

Callback / batch variants: executeByTypeWithCallback, executeBatchByType, executeBatchByTypeWithCallback (each batch is single-type). All are payable.

  • Quoting stays typed via the MultiReactorOrderQuoter (quoteExclusiveDutch / quoteV3Dutch / quoteV4Dutch); there is no by-type quote.
  • Cancellation is unchanged from today's flow (the by-type entry points do not affect it).
  • Plain execute / quote are disabled and revert UseTypedExecute(). Wrong type → safe signature-mismatch revert; unknown reactorType → UnknownReactorType(uint16).

Executor contract

Deploy your own executor contract implementing IReactorCallback. With the Router, the executor targets a single Router address per family instead of many per-version reactors.

New path — target the Router (preferred)

  • Bind / whitelist the Router address (one per family) and call its typed by-type entry point from your executor: Router.executeByTypeWithCallback(reactorType, signedOrder, callbackData) (or the type-prefixed execute{Type}WithCallback).
  • In reactorCallback(resolvedOrders, callbackData), msg.sender is the Router — approve msg.sender for output tokens (never a hardcoded address). One approval covers all order types.

Legacy path — per-reactor whitelist (in-flight orders only)

For orders already signed against the old standalone reactors, the previous model still applies: the executor maintains a reactor whitelist; execute(reactor, signedOrder, callbackData) / executeBatch take the target reactor as the first argument and call reactor.executeWithCallback; the owner must call setReactorWhitelisted(reactor, true) first (else ReactorNotWhitelisted). New orders route through the Router path above.

Reference implementations:

Query open orders

List orders for a chain, filler, or swapper. Market makers poll this to discover fillable orders (e.g. open orders during the Dutch-auction window) and to reconcile fills. Results are cursor-paginated.

GET https://sgp1.test.x.pancakeswap.com/order-handler/orders

Examples

Query open orders exclusive to a filler:

GET https://sgp1.test.x.pancakeswap.com/order-handler/orders?chainId={chainId}&exclusiveFiller={contractAddress}&orderStatus=OPEN&limit=10&cursor={hash}

Query all open orders on a chain:

GET https://sgp1.test.x.pancakeswap.com/order-handler/orders?chainId={chainId}&orderStatus=OPEN&limit=10

Query parameters

All parameters are optional individually, but they must form one of the valid combinations below.

ParamTypeDefaultDescription
limitnumber5Page size. Positive integer, 1–20 (max 20)
orderStatusstring-One of OPEN, PENDING, FILLED, EXPIRED, CANCELLED — see Order status values
chainIdnumber-Supported chain ID (e.g. 1, 56, 42161)
offererstring-Swapper address. Combine with chainId or orderStatus
fillerstring-Filler address (who filled / will fill). Combine with chainId or orderStatus
exclusiveFillerstring-MM address with exclusive rights. Combine with chainId or orderStatus
orderHashstring-Single 0x order hash (66 chars). Returns just that order; other filters ignored
orderHashesstring-Comma-separated list of 0x order hashes. Returns those orders; other filters ignored
cursorstring-Pagination cursor — the hash of the last order from the previous page. That order is skipped; results continue after it
descbooleanfalseSort by createdAt. false = oldest first; true = newest first

Valid filter combinations

Pass one of these, or the request fails with 500 INTERNAL_ERROR and detail Invalid query params on orders:

  • orderHashes (alone)
  • orderHash (alone)
  • offerer + (chainId or orderStatus)
  • filler + (chainId or orderStatus)
  • exclusiveFiller + (chainId or orderStatus)
  • chainId + orderStatus

You may add further filters to narrow a query (e.g. exclusiveFiller + chainId + orderStatus). When orderHash or orderHashes is present it takes precedence and the other filters are ignored.

Pagination

  • Default page size is 5, maximum 20 (limit).
  • Orders are sorted by createdAt — ascending by default, descending when desc=true.
  • To fetch the next page, take the hash of the last order in the current response and pass it as cursor. The cursor order itself is skipped, so the next page starts with the following order.
  • There is no nextCursor field in the response; when fewer than limit orders are returned you have reached the end.

Response

Returns 200 with an orders array. Each order is the stored order minus internal fields.

{
  "orders": [
    {
      "type": "EXCLUSIVE_DUTCH_ORDER",
      "encodeOrder": "0x0000...",
      "signature": "0x...",
      "nonce": "1",
      "hash": "0x...",
      "chainId": 56,
      "status": "OPEN",
      "offerer": "0x...",
      "input": {
        "token": "0x...",
        "startAmount": "10000000000000000",
        "endAmount": "50000000000000000"
      },
      "outputs": [
        {
          "token": "0x...",
          "startAmount": "10000000000000000",
          "endAmount": "10000000000000000",
          "takingAmount": null,
          "recipient": "0x..."
        }
      ],
      "reactor": "0xcfe2a565072f85381775eb12644d297bf0f66773",
      "deadline": "2026-06-22T15:30:00.000Z",
      "decayStartTime": "2026-06-22T14:30:00.000Z",
      "decayEndTime": "2026-06-22T15:00:00.000Z",
      "filler": null,
      "exclusiveFiller": "0x...",
      "exclusivityOverrideBps": "100",
      "quoteId": null,
      "transactionHash": null,
      "blockNumber": null,
      "createdAt": "2026-06-22T14:00:00.000Z",
      "updatedAt": "2026-06-22T14:00:00.000Z"
    }
  ]
}
Response fields
FieldTypeDescription
typestringEXCLUSIVE_DUTCH_ORDER, EXCLUSIVE_DUTCH_ORDER_V3, DUTCH_ORDER, or LIMIT_ORDER
encodeOrderstringABI-encoded order bytes (note the field name — no "d")
signaturestringUser (or cosigned) order signature
noncestringSwapper nonce
hashstringUnique order hash. Use as the cursor value and for status lookup
chainIdnumberChain ID
statusstringOPEN, PENDING, FILLED, EXPIRED, or CANCELLED — see Order status values. PENDING means a fill transaction is on-chain but not yet finalized
offererstringSwapper address
inputobject{ token, startAmount, endAmount } — amounts are stringified uint256
outputsarray{ token, startAmount, endAmount, takingAmount?, recipient } per output
reactorstringReactor contract address
deadlinestringOrder deadline (ISO-8601)
decayStartTimestring or nullDutch decay start (ISO-8601)
decayEndTimestring or nullDutch decay end (ISO-8601)
fillerstring or nullActual filler address once filled
exclusiveFillerstring or nullMM address with the exclusivity window
exclusivityOverrideBpsstring or nullOverride surcharge in bps (stringified)
quoteIdstring or nullRFQ quote ID, if the order came from a quote
transactionHashstring or nullFill transaction hash once filled
blockNumberstring or nullFill block number once filled
createdAtstringCreation timestamp (ISO-8601)
updatedAtstringLast-update timestamp (ISO-8601)

Error responses

HTTPBodyCause
400Zod issues arrayQuery validation failed — malformed address, hash, or cursor; unsupported chainId; invalid orderStatus; or limit outside 1–20
500{ "errorCode": "INTERNAL_ERROR", "detail": "..." }Invalid filter combination (detail = Invalid query params on orders) or a server/database fault

To confirm a fill on-chain, see Confirm order — it documents the reactor's Fill event and the GET /order/:chainId/:hash status endpoint.