Simple Flow of PCSX

  1. The Filler pushes their order book to PCSX Order Price API through Websocket
  2. The PCSX Order Price API replies the Swapper with the best order quote
  3. The Swapper approves Permit2 to move their funds.
  4. The Swapper signs the order, including the Permit2 signature and submit to the PCSX Order Handler API
  5. The Filler receives the order from PCSX Kafka, check if the order is correct and profitable, and submit to the Filler contract(Executor)
  6. The Executor calls the Reactor executeWithCallback with the order info.
  7. The Reactor checks the order info and the signature, and transfers the input token from the Swapper to the Executor. (PCSX does not native token as input)
  8. The Reactor then calls the Executor reactorCallback and performs the Executor swap logic.
  9. The Executor should allow the Reactor to transfer the output token from the Executor to the Reactor. (Or transfer the native token to the Reactor at the end of the swap logic)
  10. The Reactor transfer the output tokens from Executor to its own, checks if the output amount is correct or not and transfer the output token to the corresponding recipients.

Exclusivity

Order Time Person eligible to fill
User sign the order → decayStartTime exclusive filler or filler who is willing to pay the exclusivityOverrideBps can fill
decayStartTime → decayEndTime (decay starts) anyone can filler
decayEndTime → deadline (decay stops) anyone can filler

Native Token n Support

PCSX only supports Wrapped native token as input. When user swap from native tokens to any tokens, our frontend will wrap the native token, get quote on the Wrap native token and send the token to the pcsx contracts. Therefore, in order to maximise the gain, we suggest all Market Makers to support both native token and Wrap native token pairs.

Quote Order Price

Get order price

POST <https://sgp1.test.x.pancakeswap.com/order-price/get-price?filler=0x>..

Body:

{
  "tokenInChainId": 56,
  "tokenIn": "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82",
  "tokenOutChainId": 56,
  "tokenOut": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
  "amount": "1000000000000000000" //In wei,
  "type": "EXACT_INPUT",
  "slippageTolerance": "0.01", // Optional
  "configs": [
    {
      "routingType": "DUTCH_LIMIT",
      "swapper": "0x1111111111111111111111111111111111111111", // User
      "exclusivityOverrideBps": 100, // Optional
      "startTimeBufferSecs": 60, // Optional
      "auctionPeriodSecs": 3600, // Optional
      "deadlineBufferSecs": 120 // Optional
    }
  ]
}

Next response is more pseudo JSON to illustrate format

Response:

{
    "messageType": "PRICE_RESPONSE",
    "message": {
        "bestOrder": DutchOrder,
        "allPossibleOrders": [DutchOrder]
    }
}

Response that demonstrate order based on MM price calculation

{
    "type": "DUTCH_LIMIT",
    "order": {
        "quoteId": "uniqueQuoteId123",
        "requestId": "7b19d45d-590d-4d8d-b314-8bd22d449cdd",
        "startTimeBufferSecs": 60,
        "auctionPeriodSecs": 3600,
        "deadlineBufferSecs": 120,
        "slippageTolerance": "0.5",
        "permitData": {
            "domain": {
                "name": "Permit2",
                "chainId": 97,
                "verifyingContract": "0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768"
            }
            ...
        },
        "orderInfo": {
            "reactor": "0xCfe2a565072f85381775Eb12644d297bf0F66773",
            "swapper": "0x1111111111111111111111111111111111111111",
            "nonce": "7786519691593674576434758944928673618219620429447126630005716426566992061",
            "deadline": "1712069517",
            "additionalValidationContract": "0x0000000000000000000000000000000000000000",
            "additionalValidationData": "0x00",
            "decayStartTime": "1712065797",
            "decayEndTime": "1712069397",
            "exclusiveFiller": "0x3541a2456E40324Be9c3488755f61e984dECEDf6",
            "exclusivityOverrideBps": "100",
            "input": {
                "token": "0x9f063C8ED6cF9c454993677fd53b2AF572c91d6F",
                "startAmount": "5000000000000000000",
                "endAmount": "10000000000000000000"
            },
            "outputs": [
                {
                    "token": "0x1F3dAD92459B65f98a206D55dbE9F21673C2815A",
                    "startAmount": "5000000000000000000",
                    "endAmount": "5000000000000000000",
                    "recipient": "0xfa4E489A8bB0F7D3a13Ab5352f72981A24c7Cb84"
                }
            ]
        },
        "encodedOrder": "0x0000..."
    }
}

Submit Price (part of MM integration)

For communication we gonna use WebSockets. When connection to WebSocket add header x-mm-secret=(check with us) for authorization.

Order Book

CONNECT wss://sgp1.test.x.pancakeswap.com/quote-service/order-book?networkId=1 Where networkId is required parameter. And start streaming order books of tokens you support.

Include you fee into OrderBook level. For gas usage, you can use baseTokenGas field in each OB, so we deduct that amount from output.

Send messages with some frequency. The TTL for OrderBook is 10 sec. When the market maker submits their order book to the quote api, the api assumes that order book is sorted by the amount in ascending order. Hence, the api will treat the first level in bid/ask array as the minimum amount that the market maker is willing to start filling. On the other hand, the last level in the bid/ask array will be the maximum amount.

Here is an example of CAKE-USDT order book:

{
    "orderBook": [
        {
		        // MUST BE IN ASCENDING ORDER
            "bid": [
		            // level 1
                ["10","1.68"], // amount in CAKE, rate to USDT
                // level 2
                ["20","1.67"],
                // level 3
                ["30","1.66"],
                ...
            ],
            // MUST BE IN ASCENDING ORDER
            "ask": [
                ["10","1.68"], // amount in CAKE, rate to USDT
                ["20","1.67"],
                ["30","1.66"],
								...
            ],
            // CAKE
            "baseToken": "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82",
            "baseTokenGas": "0.1",// gas cost in baseToken (optional)
            //USDT
            "quoteToken": "0x55d398326f99059ff775485246999027b3197955",
            "minimum": "10"// should match your bid/ask min amount
        },
        {
		        // MUST BE IN ASCENDING ORDER
            "bid": [
                ...
            ],
            // MUST BE IN ASCENDING ORDER
            "ask": [
								 // level 1
                ["10","0.595"], // amount in USDT, rate to CAKE
                // level 2
                ["20","0.591"],
                // level 3
                ["30","0.602"],
            ],
            // USDT
            "baseToken": "0x55d398326f99059ff775485246999027b3197955",
            "baseTokenGas": "0.16",// gas cost in baseToken (optional)
            // CAKE
            "quoteToken": "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82",
            "minimum": "10"// should match your bid/ask min amount
        }
    ],
    "quoteId": "mm-timestamp-orn-usdt"
}

Given the above order book example, here is how we send out quotes on CAKE-USDT

  1. Choose the correct bid/ask

  2. Calculate the result amount

    Given the case: User wants to swap an exact input of CAKE to USDT, the api takes the bid on CAKE(baseToken) - USDT(quoteToken)

    Here is the pseudo code on the calculation:

    sizeFilled = 0
    resultAmount = 0
    
    for (i = 0; i < levels.length; i++) {
    	levelSize = levels[i][0]
    	levelPrice = levels[i][1]
    	partFillSize = size - sizeFilled
    	if (levelSize >= size) {
    		 resultAmount += levelPrice * partFillSize / size
    		 break 
    	}
    	resultAmount += (levelPrice * (levelSize - sizeFilled)) / size
    	sizeFilled = levelSize
    }
    
    if (resultAmount > 0) {
     return resultAmount
    }
    
  3. Add gas

Submit Order

Order Submission API

BSC Testnet:

BSC Mainnet:

Sepolia:

Arbitrum:

Etherum:

Install:

yarn add @pancakeswap/permit2-sdk @pancakeswap/pcsx-sdk

Here is an example on constructing an exclusive order.

import { REACTOR_ADDRESS_MAPPING, ExclusiveDutchOrder, ExclusiveDutchOrderInfoJSON } from '@pancakeswap/pcsx-sdk'

const chainId = 56 // or 97 depend on bsc mainnet or testnet
const orderInfo: ExclusiveDutchOrderInfoJSON = {
  reactor: REACTOR_ADDRESS_MAPPING[chainId],
  swapper: "0x0...",
  nonce: 1, // depends on the permit2
  deadline: 1712042224, // unix time
  additionalValidationContract: "0x0000000000000000000000000000000000000000", // if any
  additionalValidationData: "0x00", // if any
  decayStartTime: 1712042164, // unix time
  decayEndTime: 1712042224, // unix time
  exclusiveFiller: "0x...", // the filler contract address
  exclusivityOverrideBps: 100,
  input: {
    token: "0x...", // input token address
    startAmount: 10000000000000000, // in smallest unit
    endAmount: 50000000000000000, // in smallest unit
  },
  outputs: [
    {
      token: "0x...", // output token address
      startAmount: 10000000000000000n, // in smallest unit
      endAmount: 10000000000000000n, // in smallest unit
      recipient: "0x...",
    },
  ],
};

const order = ExclusiveDutchOrder.fromJSON(orderInfo, chainId);

Generate the signature and encode order:

const permitData = order.permitData();
const signature = await swapper.signTypedData({
  domain: {
    name: permitData.domain.name,
    version: permitData.domain.version,
    verifyingContract: permitData.domain.verifyingContract,
    chainId,
  },
  types: permitData.types,
  primaryType: "PermitWitnessTransferFrom",
  message: {
    ...permitData.values,
  },
});

const encodedOrder = order.encode();

Approve permit2 address on user spending:

ERC20(0x...).forceApprove(
  0x..., // swapper address
  0x31c2f6fcff4f8759b3bd5bf0e1084a055615c768,
  type(uint256).max
);

User submit order through :

POST <https://sgp1.test.x.pancakeswap.com/order-handler/order>

Post body:

{
    "encodedOrder": "0x0...",
    "signature": "0x0....",
    "chainId": 56 // or 97 depend on bsc mainnet or testnet
}

Response:

{ 
	"hash": "0x..."
}

The hash is unique id of the order by each chain and will be included in the fill event and used to check the order status.