AugustusRFQ API Specification

Want to become a market maker on ParaSwap? This document describes the API you need to provide to us to make that happen!

Objectives

This is intended to be a common API format for all market makers to use, substantially reducing the amount of work we have to do to integrate a new market maker, and most of the back and forth agreeing a protocol between us.

This specification is newly created for new market makers wanting to integrate against ParaSwap via our efficient AugustusRFQ protocol smart contracts.

How does it work?

In ParaSwap, market makers are enabled by them providing us with an API (one for each chain) to get information on what tokens they support, which base/quote pairs, and the price grid containing something akin to an order book with lists of bids and asks defining the pricing curve.

This information is regularly requested to keep the prices up to date. To enable more control on update of prices, and also save bandwidth potentially, it is recommended to support streaming price grid updates to us via WebSockets.

When a user on ParaSwap requests pricing, we go through all DEXs and market makers, computing prices. For market makers, this means using the previously cached price grid. We cannot do a request for (indicative) quote on each user individually, as that would make a huge volume of requests and add unwanted latency.

When a user comes to actually make a swap using a market maker, we then contact the market maker for a firm quote, in the form of a signed order, which can be executed on chain using ParaSwap's AugustusRFQ contract. This may be requested for an amount in either the bought or sold asset.

Users may be blacklisted by a market maker for making too many swap requests without actually executing the swap on chain. To avoid issues, we request market makers to provide a list of blacklisted users. This allows us not to provide pricing to that user for the market maker that has blacklisted them, and prevents subsequent failure on transaction building.

Should a market maker not fulfill their promise to provide a signed order for a given amount when requested, or give a bad price in the order that is returned (i.e. deviating from the price they have given in the price grid) they may be punished by having their market making disabled in ParaSwap. This may also happen if the signature cannot be validated (as either EOA or via smart contract), there is insufficient allowance to AugustusRFQ contract, or insufficient funds in the maker wallet.

Endpoints

All regular HTTP endpoints (except WebSockets) should have a common base URL that we can use to make requests. These endpoints must be secured using the procedures described below, however they may also employ IP whitelisting of ParaSwap servers for additional security. All endpoints should return JSON data as a response at all times, even if there is an error. All error responses should return an unsuccessful HTTP response code (4xx or 5xx) and in the body a JSON object with the key "error" containing a string describing the error. All non-error responses may include a "message" key in the top level object, alongside the normal payload data, the contents of which will be logged on ParaSwap servers for any request.

A WebSocket API is optional and may be configured on a different URL than the other endpoints (but it could also use the same URL with /ws at the end, as you prefer...).

Authentication (optional)

There are three key pieces of information we require from a market maker for security purposes:

  • Domain - this is a string you can use to identify traffic coming from us (in case you want to enable market making with someone else), and optionally to differentiate between production, staging and test. In the simplest case this can simply be the string "paraswap".

  • Access Key - this is a string that we will send back to you in request headers as a kind of passcode to quickly check that the request is coming from us. It may be different for each domain and it should not be shared with anyone except ParaSwap.

  • Secret Key - this is a string used by us to authenticate our requests to your servers as detailed below. It may be different for each domain and it should not be shared with anyone except ParaSwap.

In order to authenticate our requests we will do the following:

  1. Get the current timestamp as milliseconds since Unix epoch (i.e. that returned by Date.now() in JavaScript)

  2. Compute the payload to be signed by concatenating the following (with no spaces or other separator between):

    1. Timestamp (as a string in decimal form)

    2. HTTP method in UPPERCASE (will be GET or POST)

    3. Path of the request (the part after the domain, beginning with a /) e.g. /prices. Note that if the base URL already contains a path e.g. /mainnet it will be included.

    4. Query parameters that were passed in the URL exactly as given including the initial ? e.g. ?base=WETH&quote=USDC. If there are no query parameters, use an empty string instead. Note that none of the endpoints in this specification use query parameters, so this part will normally be an empty string.

    5. If present, the payload sent in a e.g. POST request, exactly as sent and received. This will always be a JSON object in this specification, enclosed by {}. If no payload e.g. for GET request, use an empty string.

    So a full payload may look something like this (if the URL used is e.g. https://pspool.org/endpoint?key=value): 1234512345123POST/endpoint?key=value{"amount":"123"} For WebSockets connection opening we assume current timestamp, method of GET, the path of the URL used to connect, no query parameters and no payload.

  3. Compute the HMAC-SHA256 of this payload in hexadecimal using the Secret Key as the key. We would do this using Node.js crypto library (can be used as a reference) e.g. const { createHmac } = require('crypto'); const hmac = createHmac('sha256', '<secret key>'); hmac.update('<payload>'); console.log(hmac.digest('hex'));

  4. Attach the following headers to the request:

    • X-AUTH-DOMAIN - the Domain as described earlier

    • X-AUTH-ACCESS-KEY - the Access Key specified for the Domain

    • X-AUTH-TIMESTAMP - the timestamp used to create the signature

    • X-AUTH-SIGNATURE - the HMAC-SHA256 computed in the previous step

It is mandatory for the market maker to verify all these headers are correct/matching and the timestamp is sufficiently close to the current time (e.g. +/- 30 seconds). Otherwise, the request should be rejected with an appropriate error message to describe the issue.

Tokens endpoint

Path: /tokens

Method: GET

This endpoint provides details of all tokens used by the market maker. The response should be a JSON object with the key "tokens", which in turn contains an object containing a number of entries, the key of which is a Token ID (recommended to use the token's symbol if it is unique), and the value is an object with the following fields (most are just for informational purposes):

  • "symbol" (string) - the token's symbol as determined by the market maker (should probably be the same as the one provided by the token's contract)

  • "name" (string) - a full name for the token as determined by the market maker (should probably be the same as the one provided by the token's contract)

  • "description" (string) - a description of the token, noting any important details where appropriate e.g. if it has been deprecated in favour of another token

  • "address" (string) - the address of the token (case is ignored and normalized to lowercase on our side)

  • "decimals" (number) - the number of decimals this token has i.e. which power of 10 value represents one whole token when it is processed on chain, normally the value returned by the decimals() method of the token's contract

  • "type" (string) - type of token, for now we only support "ERC20" for market making (but we may also support "ERC1155" or "ERC721" in future since they are implemented in the smart contract)

This is an example of what to return from this endpoint (but without the newlines, indentation, spacing etc. that are not required, and given here to improve readability):

{
  "tokens": {
    "WETH": {
      "symbol": "WETH",
      "name": "Wrapped Ether",
      "description": "Canonical wrapped Ether on Ethereum mainnet",
      "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
      "decimals": 18,
      "type": "ERC20"
    },
    "USDC": {
      "symbol": "USDC",
      "name": "USD Coin",
      "description": "Popular US dollar stablecoin, provided by Circle",
      "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "decimals": 6,
      "type": "ERC20"
    }
  }
}

Pairs endpoint

Path: /pairs

Method: GET

This endpoint provides a list of trading pairs supported by this market maker (or recently disabled), mapping Pair IDs to base and quote Token IDs, and also giving an estimated indication of liquidity in USD terms associated with this pair (this is used to assist with finding routes between tokens). Pairs should normally only be given in one direction of base vs. quote since the pricing of the other direction can be easily derived.

The response should be a JSON object containing the key "pairs", which in turn contains a JSON object containing a number of entries, the key of which is the Pair ID, and the value being a JSON object with the following:

  • "base" (string) - Token ID representing the base token of this pair (the token which the bids/asks are for)

  • "quote" (string) - Token ID representing the quote token of this pair (the token in which prices are given)

  • "liquidityUSD" (number) - an estimate of the liquidity the market maker has backing this pair, in US dollar terms (could be the maximum the market maker is willing to sell in both tokens, converted to USD and added together)

The Pair ID is recommended to be base token ID/symbol followed by / then the quote token ID/symbol. Here is an example payload returned from this endpoint (again ignoring newlines/indentation/spacing):

{
  "pairs": {
    "WETH/USDC": {
      "base": "WETH",
      "quote": "USDC",
      "liquidityUSD": 468000
    },
    "WETH/USDT": {
      "base": "WETH",
      "quote": "USDT",
      "liquidityUSD": 512500
    },
    "WBTC/WETH": {
      "base": "WBTC",
      "quote": "WETH",
      "liquidityUSD": 129800
    }
  }
}

Prices endpoint

Path: /prices

Method: GET

This endpoint provides the full grid of price curves for all the supported pairs, which ParaSwap will store in its cache, and can also specify pairs to remove from the cache, or even restrict trading to a single direction.

The response should be a JSON object containing the key "prices", which in turn contains a JSON object with a number of entries, with the key being a Pair ID, and the value being an object which may contain the keys "bids" and "asks" (omitting either key means that trading in that direction is disabled and the corresponding cache entry will be cleared; to disable the pair entirely, both can be omitted so the object is empty {}).

Both "bids" and "asks" if present, should consist of an array of [price, amount] tuples defining the price curve as an order book, where price is a string representing the price of one base token in terms of the quote token, and amount is a string representing the amount of base token for this entry. We use bignumber.js library to parse these strings as decimal numbers with high precision. When computing a price the "orders" (tuples) are filled in sequential order, until the desired amount is reached. As such it is expected that the entries start with the best price and get progressively worse (however this is not enforced, so any price curve can be used). It is also expected that the best ask price is above the best bid price (this is not enforced neither, so market makers can risk arbitrage to deliver market beating prices and/or accelerate trading volume if they so wish).

Here is an example payload (newlines/indentation/spacing may be removed in the real thing):

{
  "prices": {
    "WETH/USDC": {
      "bids": [
        ["1540", "0.5"],
        ["1500", "1.5"],
        ["1480", "3"]
      ],
      "asks": [
        ["1560", "1"],
        ["1580", "1.5"],
        ["1600", "2"],
        ["1650", "9"]
      ]
    },
    "WETH/USDT": {}
  }
}

In this example, trading for WETH/USDT is disabled (the cached prices for it will be cleared). A user selling 1.5 WETH to this market maker should receive 1540 * 0.5 + 1500 * 1 = 2270 USDC (average price 1513.333 USDC), whereas buying 10 WETH from the market maker, they would spend 1560 * 1 + 1580 * 1.5 + 1600 * 2 + 1650 * 5.5 = 16205 USDC (average price 1620.5 USDC). For computing prices based on a quote token amount (USDC in this case) we invert the order book (bids become asks and vice versa, new price is computed as 1 / price, and new amount is price * amount), then use the same process.

Note that if you wish to remove a pair from trading, it should not be removed from /pairs endpoint straight away. Instead, it must be retained there and given prices of {} as shown above for WETH/USDT for a period of time, after which it can finally be removed.

Failing to respond to this endpoint or responding with an error will cause market making to be temporarily disabled for this market maker, which may be done intentionally if you wish to cease trading entirely for a period of time.

Firm quotes endpoint

Path: /firm

Method: POST

This endpoint is used to obtain a firm rate from the market maker, which is represented as a signed order for the AugustusRFQ smart contract.

For use in the ParaSwap system the taker is set as a ParaSwap contract (depending on execution context it can be Augustus V5, Augustus V6 or another contract) and the user of ParaSwap (the msg.sender to AugustusSwapper) is encoded as metadata in the nonceAndMeta field; AugustusSwapper or other contracts are programmed to always check the user and metadata are matching when interacting with AugustusRFQ, so that no one can steal the order from the user and importantly to avoid users spamming requests for firm quotes, since they have to use a real user address and this address can be blacklisted by the market maker if they do so.

The body of the POST request will contain the following in a JSON object:

  • "makerAsset" (string) - address of the token the maker is selling

  • "takerAsset" (string) - address of the token the taker is selling

  • "makerAmount" or "takerAmount" (string) - amount of the maker or taker asset respectively that is to be traded (only one may be specified and the other is calculated by the market maker based on current prices), this is specified as an integer by scaling up by 10^decimals

  • "userAddress" (string) - address of the user as described above

  • "takerAddress" (string) - address of the ParaSwap contract that fills the order passed by ParaSwap on calling POST /firm (either Augustus or another contract depending on the execution context. For V5 it's always AugustusV5, for V6 it's either AugustusV6 or Executors). These addresses can be verified in the documentation here or by the API endpoint here.

Here is an example payload sent to market maker (will be sent without newlines/indentation/spacing, the case of alphabetic characters in addresses is not specified, it may be all lowercase/uppercase or checksum encoded, the market maker should normalize it to the desired format):

{
  "makerAsset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "takerAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
  "takerAmount": "1500000000000000000",
  "userAddress": "0x05182E579FDfCf69E4390c3411D8FeA1fb6467cf",
  "takerAddress": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57"
}

This request represents a user selling 1.5 WETH for USDC.

Note that the specified amount specified in taker or maker asset may be slightly larger than the amount the user is able to swap, so that there is some flexibility if e.g. it is part of a multihop and the user receives less/more of the taker asset from a previous swap.

The response to this request should be a JSON object containing the key "order", which in turn contains a JSON object with the following fields:

  • "nonceAndMeta" (string) - a number (uint256) which encodes the user's address in the lower 160 bits and is otherwise filled with random/unpredictable data i.e. to calculate this, convert the address to an integer, and add it to a random integer shifted left by 160 bits (the random integer must be less than 2^96), e.g. in Python (random.randint(0, 2**96 - 1) << 160) + int(address, 16)

  • "expiry" (number) - the time in seconds past UNIX epoch at which this order becomes no longer executable, this should be at least 2 minutes in the future

  • "makerAsset" (string) - same as one passed in

  • "takerAsset" (string) - same as one passed in

  • "maker" (string) - this is the address containing the market maker's funds and it must have approved the AugustusRFQ contract to spend them, it is also the signer of the order if it is an EOA, otherwise it is a smart contract implementing EIP-1271

  • "taker" (string) - the address of the contract that fills the order which can be picked from the request payload (either Augustus or another contract depending on the execution context. Augustus addresses can be verified here)

  • "makerAmount" (string) - same as that passed in or calculated from the taker amount

  • "takerAmount" (string) - same as that passed in or calculated from the maker amount

  • "signature" (string) - 0x followed by any number of bytes encoded in hexadecimal (lowercased) representing the bytes signature in the AugustusRFQ smart contract (the order hash of all other fields listed here is computed using EIP-712, and this must be signed using either an EOA or a smart contract EIP-1271 signature)

For creating the signature using an EOA maker address, please refer to the ParaSwap SDKs (TypeScript or Python) for sample code or even consider using them directly. For example in the Python SDK you can use the function create_managed_p2p_order to create a suitable order and OrderHelper.sign_order to sign it using web3. If you use a EIP-1271 signature it depends on your smart contract how the signing will work, so you may need custom code.

Here is an example response payload (yet again ignore newlines/indentation/spacing):

{
  "order": {
    "nonceAndMeta": "77194726158210796949047323338180021686013833221005105572687668833110133598159",
    "expiry": 1667344557,
    "makerAsset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "takerAsset": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
    "maker": "0x7777777777777777777777777777777777777777",
    "taker": "0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57",
    "makerAmount": "2270000000",
    "takerAmount": "1500000000000000000",
    "signature": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcc"
  }
}

This payload corresponds to previously given examples regarding assets and prices etc.

If the request cannot be processed because the user address is blacklisted, and only in this case, you can return a successful response without any "order" key. Optionally, this can contain a "message" field explaining that the user was blacklisted and even having a reason why. When a user is detected blacklisted in this way it is added to the cache just as if it was received in the /blacklist endpoint described below.

You could also use our SDK for order signing. Please note, that you should pass takerAddress from the payload (whitelisted ParaSwap contract) as contractTaker, and userAddress (actual user address) as taker, as described in the example here.

Blacklist endpoint

Path: /blacklist

Method: GET

This endpoint is used to list any user addresses which are currently blacklisted. The payload is simply a JSON object with a key "blacklist" containing an array of blacklisted user addresses. This should not contain duplicate entries! Here is an example:

{
  "blacklist": [
    "0x05182E579FDfCf69E4390c3411D8FeA1fb6467cf",
    "0x0000000000000000000000000000000000000000"
  ]
}

WebSocket connection (optional)

Market makers may optionally provide a WebSocket interface, in addition to the other endpoints. This is a one way channel from market maker to ParaSwap, intended to serve pricing updates more efficiently. Each message is a JSON object, and it can be used to send any information from other endpoints at any time, except for "order", however instead of sending the complete data each time (except for the very first message), it sends updates (creating or updating entries only, not deleting them). Here are specific details for each possible key:

  • "tokens" should contain details of any newly supported tokens or corrections to existing ones

  • "pairs" should contain any newly added pairs or corrections to existing ones, it is not recommended to update "liquidityUSD" by WebSockets since it is not used from this source and it is anyhow constantly changing

  • "prices" should contain any pairs which need their prices updated, refreshed in the cache (after not being updated for too long), or that should be removed from the cache (bids, asks or both)

  • "blacklist" should contain only newly added user addresses

  • "message" can be used at any time to cause information to be logged on ParaSwap servers

When the WebSocket is connected the first message should contain the complete responses to /tokens, /pairs, /prices and /blacklist endpoints in one JSON object (so that they don't need to be requested separately and potentially introduce synchronization issue). Once the connection is established the /blacklist endpoint will still be queried periodically in order to refresh the blacklisted status of all blacklisted users, which is only stored in cache temporarily.

Should the market maker wish to stop trading entirely, the websocket connection can simply be disconnected and refuse to reconnect until they wish to start trading again.

Last updated