Links

Getting Started

So you want to market make on Hashflow? Awesome! This doc will walk you through the steps of integrating with Hashflow so that you can start offering quotes and making trades. Before you walk through this tutorial, however, you need to reach out to the Hashflow team to set up your Market Maker credentials.

1. Connect to WebSocket

Check out this example repo which contains example files showing how to connect to our Websocket. These files are only an illustrative example. You can implement your own market maker in whatever language you'd like.
A few things to note here:
  1. 1.
    This repo is only a skeleton. For it to work correctly, you need to add in your own data and logic at all the places indicated with // TODO statements.
  2. 2.
    The market maker name 'TestMM' is only a placeholder. Connecting with this name will NOT work. On the WebSocket side, we only permit allowlisted Makers. Reach out to us (Telegram/Discord) with your market maker name and we'll add it to the allowlist. After that, replace 'TestMM' with your market maker.
  3. 3.
    Our WebSocket validates connections based on the marketmaker field provided. Each maker has a specific authentication key. This key needs to be passing the Authorization header of the WS request. We additionally support restricting access to only specific IP addresses for extra security. Contact us (Telegram/Discord) to have your market maker credentials verified.
  4. 4.
    The marketmaker field needs to be an exact (capitalization) match. If we allowlist 'MyMarketMaker' and you try connecting with 'mymarketmaker', our WebSocket will reject the request. For this reason, we use CamelCase notation for our MM names. Acronyms should still be capitalized (so 'ABCMaker' instead of AbcMaker)

2. Create a Pool

Now that you've connected to the WebServer, you'll need a Pool to offer quotes.
The first step is to have your wallet added to the allowlist for Pool Creation. The Hashflow team will be able to assist with this step. Once that is completed, you will be able to see the "Create Pool" button on the Pools page.
Navigate to Hashflow -> "My Pools" and follow the instructions there to create a pool.
You'll have to fill in three fields:
  1. 1.
    Pool/Token Name: The name you'd like to give your pool.
  2. 2.
    Signer Address: The key you'll use to sign quotes off-chain for your market maker. Put the public key (address) here and ensure the private key is stored securely. You'll need to be able to programmatically sign quotes with the private key later on (in the signQuote API). Make sure this key is not in cold storage.
  3. 3.
    Public Pool: Whether you want your pool to be public (LPs can add liquidity) or private. Public pools require LPs to have their deposits signed by you (we will cover this later on).
After submitting the transaction, your new pool will be created. It may take a few moments for the Hashflow backend to index the on-chain transaction and show you the pool in the UI. You can refresh the page after 30 seconds and you should then see something like:
If you've created a private pool, you have two options for how to fund market making:
  1. 1.
    External account (EOA or Smart Contract). You can use an external account to fund market making out of your pool. For this, you need to specify the eoa field when sending quotes (see API below). The advantage of External Account trading is that you don't need to lock funds into a pool. The disadvantages are higher gas fees, due to native token (e.g. ETH <-> WETH) auto-conversions.
  2. 2.
    Pool Funds. You can also use pool funds to market make. You can add funds to your pool using the Deposit function. Funds will be credited to / debited from the pool balance if no External Account is specified. The advantage of using pool funds is that they make trading cheaper which is an advantage on aggregators, such as 1inch.
For public pools, you need to use (2) pool funds since External Accounts are not supported.
IMPORTANT: Once the Pool has been created, please reach out to the Hashflow team to add it to the watchlist.

3. Submit Price Levels (indicative pricing)

In order to indicate current pricing for Hashflow APIs and aggregators, you have to send price levels for each supported pair on every network. The recommended frequency is every second for every pair on every supported network. A pair is a tuple [chainID, baseToken, quoteToken]. This means that for ETH <-> USDC trading on Ethereum, for example, two sets of levels need to be sent. These price levels represents indicative pricing for each tuple. The format of these messages is:
{
"messageType": "priceLevels",
"message": {
"networkId": number, // e.g. 1 (mainnet), 137 (polygon)
"baseTokenName": string, // e.g. ETH
"quoteTokenName": string, // e.g. USDT
"source": "hashflow",
"levels": [
{
// string representation of the level e.g. (2.5 for 2.5 ETH)
level: string,
// string representation of the price per unit at that level
// (e.g. 3500 for "up to 2.5 ETH at 3500 USDT per ETH")
// this price is not in decimals -- it's the actual exchange rate,
// in floating point notation
price: string
}
]
}
}
NOTE: We require price levels to be sent for source: 'hashflow' at the minimum. All sources will uses these price levels by default. However, you can also send levels for specific sources (like 1inch) if you plan to price differently for them.
Hashflow then caches these price levels for a max of 5 seconds (or until new levels are published) and uses them to route RFQs. Aggregators that tap into Hashflow liquidity (e.g. 1inch) use the same levels to determine RFQ routing.
Price Levels work similar to tax brackets, and mean "the price that you get up to this amount". For example, let's consider the pair ETH -> USDC on Ethereum, and the following levels:
  • { level: "0.5", price: "1000" } -> the first 0.5 ETH will receive 1000 USDC / ETH
  • { level: "1", price: "999" } -> the next 0.5 ETH will receive 999 USDC / ETH
  • { level: "5", price: "998" } -> the next 4 ETH will receive 998 USC / ETH
For example, if someone wanted to trade 3 ETH for USDC, the price will be: 0.5 * 1000 + (1 - 0.5) * 999 + (3 - 1) * 998 = 2995.5
It also means that the maximum amount of ETH you are looking to buy as a Market Maker is 5.
Additionally, the first level is always the minimum amount you're looking to buy. In the example above, trades that are less than 0.5 ETH will not be routed to you. This is helpful for cases where hedging trades can have constant cost.
If you're looking to support arbitrarily small amounts, your first level should be 0, for example:
  • { level: "0", price: "1000" }
  • { level: "0.5", price: "1000" } -> the first 0.5 ETH will receive 1000 USDC / ETH
  • { level: "1", price: "999" } -> the next 0.5 ETH will receive 999 USDC / ETH
  • { level: "5", price: "998" } -> the next 4 ETH will receive 998 USC / ETH

3. Receive RFQ and Respond with Quote

When users want to trade, they send an RFQ (request-for-quote) to our server. The server then creates a routing strategy and sends the RFQ to market makers. When RFQs are sent, signed quotes are expected in return. This process happens, currently, in two steps. First, you will receive an rfq message, which takes the following shape:
{
"messageType": "rfq",
"message": {
// This is a unique RFQ ID -- you need to use this when sending back a quote.
"rfqId": string,
// This will be something like: hashflow, 1inch. This is useful
// since 1inch charge fees for their trades
"source": string,
// 1 for ETH L1
"networkId": number,
// Base token (the token the trader sells).
"baseToken": string, // contract address
"baseTokenName": string, // token name (e.g. USDC, ETH, ...)
"baseTokenNumDecimals": number, // token decimals (e.g. DAI: 18, USDC: 6)
// Quote token (the token the trader buys).
"quoteToken": string, // contract address
"quoteTokenName": string, // token name (e.g. USDC, ETH, ...)
"quoteTokenNumDecimals": number, // token decimals (e.g. DAI: 18, USDC: 6)
// Exactly one of the following fields will be present in the RFQ.
// If baseTokenAmount is present, quoteTokenAmount needs to be filled by the quote.
// If quoteTokenAmount is present, baseTokenAmount needs to be filled by the quore.
// Amounts are in decimals, e.g. "1000000" for 1 USDT.
"baseTokenAmount": ?string,
"quoteTokenAmount": ?string,
// The trader wallet address that will swap with the contract. This can be a proxy
// contract (e.g. 1inch)
"trader": string,
// The wallet address of the actual trader (e.g. end user wallet for 1inch).
// This is helpful in order to understand user behavior.
// If effectiveTrader is not present, you can assume that trader == effectiveTrader.
"effectiveTrader": ?string,
// How much, in basis points, will be charged to you in fees if this order goes through.
"feesBps": number
}
}
After receiving this RFQ message, you have to compute the quote you'd like to offer (based on market conditions, internal balances, etc) and return it in the following format:
{
"messageType": "quote",
"message": {
"rfqId": string, // This should be the same rfqId that was sent by the server
"pool": string, // This should be the contract address of the pool.
// This is optional. If using an external account, this should
// contain the address of the EOA or Smart Contract.
// The External Account needs to have allowance set to the Pool.
"eoa": ?string,
// Same as RFQ
"baseToken": string,
"quoteToken": string,
// Amounts are in decimals.
"baseTokenAmount": string,
"quoteTokenAmount": string,
// The unix timestamp when the quote expires, in seconds.
"quoteExpiry": number,
}
}
The Hashflow server will generally wait 350 milliseconds for a quote before it assumes that none will be provided. RFQs are only routed if Price Levels indicate liquidity, which means that every RFQ should be served with either a quote, or a message explaining why a quote is no longer available.

4. Sign Quotes

Once the Hashflow server has decided to send your quote to the trader, it will send another message asking you to sign it. The request is of the following shape:
"messageType": "signQuote",
"message": {
// The RFQ ID that generated the quote.
"rfqId": string,
"networkId": number, // The chain ID (e.g. 1 for Ethereum mainnet)
"quoteData": {
"txid": string, // Unique identifier of the quote -- different from the RFQ ID.
"pool": string,
"eoa": ?string,
"baseToken": string,
"quoteToken": string,
"baseTokenAmount": string,
"quoteTokenAmount": string,
"quoteExpiry": number,
// The account that will be executing the swap. For 1inch, this is the 1inch proxy.
"trader": string,
// Trader actually executing the swap, if different from 'trader'.
"effectiveTrader": ?string,
// The following parameter is internal to hashflow contracts.
// It is leveraged to mitigate quote replay.
"nonce": number
}
}
}
You should then produce an EIP-191 signature of the 32-byte quote hash.
See this example JavaScript for how to generate a quote hash and sign :
// Used by Maker to sign an RFQ-T quote.
static hashRFQTQuote(
quoteData: UnsignedRFQTQuoteStruct,
chainId: number
): string {
return utils.solidityKeccak256(
[
'address',
'address',
'address',
'address',
'address',
'address',
'uint256',
'uint256',
'uint256',
'uint256',
'bytes32',
'uint256',
],
[
quoteData.pool,
quoteData.trader,
quoteData.effectiveTrader ?? quoteData.trader,
quoteData.externalAccount ?? ZERO_ADDRESS,
quoteData.baseToken,
quoteData.quoteToken,
quoteData.maxBaseTokenAmount,
quoteData.maxQuoteTokenAmount,
quoteData.nonce,
quoteData.quoteExpiry,
quoteData.txid,
chainId,
]
);
}
const signer = ... // ethers.js based signer
const hash = hashRFQTQuote(quoteData, chainId);
const signature = await signer.signMessage(
Buffer.from(quoteHash.slice(2), 'hex')
);
The helper functions have been omitted above for simplicity. Again, you're not required to use JavaScript –– this is simply what our examples use.
The smart contract Solidity code that checks the signature looks like this:
function hashQuoteRFQt(
address trader,
address effectiveTrader,
address externalAccount,
address baseToken,
address quoteToken,
uint256 baseTokenAmount,
uint256 quoteTokenAmount,
uint256 nonce,
uint256 expiry,
bytes32 txid
) internal view returns (bytes32) {
return
keccak256(
abi.encodePacked(
'\x19Ethereum Signed Message:\n32',
keccak256(
abi.encodePacked(
address(this),
trader,
effectiveTrader,
externalAccount,
baseToken,
quoteToken,
baseTokenAmount,
quoteTokenAmount,
nonce,
expiry,
txid,
block.chainid
)
)
)
);
}
NOTE: You can see above that the EIP-191 standard is used (the \x19Ethereum Signed Message:\n32 prefix). In the JavaScript example above, the signMessage method in ethers.js handles prefix generation for us.
NOTE: The public key (address) associated with the private key that is used for signing has to be used when creating the Hashflow pool.
You should then broadcast the signature back through the WebSocket:
{
"messageType": "signature",
"message": {
"txid": string, // This is the quote txid previously sent.
"signature": string // 0x-prefix signature (65 bytes)
}
}
The trader will then be able to take the signature and call Hashflow's smart contract to execute the quote (within the expiry window).

5. Subscribe to Trades

Hashflow monitors all trades automatically in our backend (for pools that are on the watchlist). We offer the option for makers to subscribe to trades on their pools for faster updates. If you want to be able to do so, please ask the Hashflow team to setup a delivery pipeline for you. If you do not require this functionality, you can skip this step.
The architecture will ensure that trade notifications will be delivered at least once. They will, however, be keyed on txid, and you should ensure that you can process them more than once in case double delivery occurs.
Once the pipeline is et up, you can subscribe to a pool's trades by sending a subscribeToTrades message over the WebSocket
{
"messageType": "subscribeToTrades",
"message": {
"networkId": number, // network id of pool
"pool": string // pool id '0x...'
}
}
If you have opted in for trade notification delivery, you HAVE TO CONTINUOUSLY SUBSCRIBE to notifications, or else the pipeline will be blocked.
Subscription is only required once per WebSocket connection. Once the subscription has been acknowledge by our server, you will receive a subscriptionAck message back.
IMPORTANT: In case of a client disconnect, the client should aim to reconnect, and send the subscribeToTrades message for each pool again, as subscriptions will not be persisted across sessions.
One way to ensure subscriptions are fresh is by sending them around every 30 seconds.
If you subscribe to trades on a pool, this means you will start getting trades messages when our backend notices a new trade. These messages will have the following format:
{
"messageType": "trade",
"message": {
"rfqId"?: string;
"rfqSource": string;
"networkId": NetworkId;
"dstNetworkId"?: NetworkId;
"txid": string;
"blockNumber": number;
"blockHash": string;
"transactionHash": string;
"blockTimestamp": number;
"baseToken": string;
"baseTokenName": string;
"baseTokenNumDecimals": number;
"quoteToken": string;
"quoteTokenName": string;
"quoteTokenNumDecimals": number;
"baseTokenAmount": string;
"quoteTokenAmount": string;
"fees": string;
"baseTokenPriceUsd": string;
"pool": string;
"dstPool"?: string;
"trader": string;
"effectiveTrader"?: string;
"status": 'filled' | 'canceled';
"feesBps"?: number;
}
}
Once you've processed this trade message (updating internal records, hedging the trade, etc), you need to respond with a tradeAck message so that our backend knows you have received the message.
{
"messageType": "tradeAck",
"message": {
"txid": string
}
}
NOTE: We require these messages to ensure successful trade processing. If you don't respond within 1h, we will treat the trade processing as failed. We have some monitoring and alerting on failed processing to ensure there are no backend issues.

6. (If using External Accounts) Set allowance for Private Pool

If using an External Account, set an allowance from the External Account to the Private Pool so that it can move funds on behalf of the external account. Allowances need to be sent for:
  • the ERC-20 tokens you plan on supporting
  • the wrapped native token (e.g. WETH) if you plan on supporting native token trades (e.g. ETH)
In the case of an ETH trade, the Hashflow Smart Contracts will automatically wrap / unwrap as necessary, so that your External Account only gets debited / credited WETH instead.
NOTE: You have to send both ETH/x and WETH/x Price Levels in order to support both types of trades
If you don't set an allowance, your quotes will fail on-chain since the pool won't be able to access the funds in the External Account.

7. Test the RFQ flow end-to-end

Once you have completed the above steps, you can test your connection in the Hashflow UI. First off, you will need to ask the team for access to an UI that connects to the Staging API.
Next up, if your indicative Price Levels are propagating properly, you should be able to see your liquidity source, as well as supported pairs in the UI.
To select the liquidity source, click on the "Gear" icon at the top of the trading terminal. You should see your obscured Market Maker name (e.g. mmX) in that list.
Next, you should unselect all liquidity sources aside from your own.
Next, select a pair and write a number in the top box. If things are working well, you should see the rfq and signQuote messages in succession, then see a quote in the UI. This means that both the Quote and the Signature have been generated.
Next, click on Trade. If things worked well, you should see your wallet successfully estimate gas, and pop up a transaction confirmation. Confirm the transaction, run the trade, and checked that everything worked out well.

Success!!

Congrats! You've successfully set up a Market Maker 🥳