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. First things first, make sure you've 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).
  5. 5.
    If you want to, we also allow you to quote with multiple order books (enabling you to quote different prices for the same assets). To do this, simply include an marketMakerIndex header in your connection request. This needs to be an integer (1, 2, 42, etc). You don't need to include the number for index 0. This is also the default case when you're only using a single orderbook.

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.

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: You can get the list of supported token using the tokens REST API. Please use the name returned from that API as baseTokenName and quoteTokenName in the priceLevels message.
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 represent "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
These levels also declare that the maximum amount of ETH you are looking to buy as a Market Maker is 5 ETH.
IMPORTANT: We require the price levels array to have 2+ entries or be empty. By providing 2+ entries, we can establish a minimum and maximum trade size you're able to quote. The exception here is the empty array, which tells us you are no longer quoting on the pair.
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 incurs a flat cost for you.
The last level is always the maximum amount you're looking to buy. In the example above, trades that are >5 ETH will not be routed to you. This is how we ensure we don't send you trades that exceed your liquidity.
If you're looking to support arbitrarily small amounts, your first level should be 0 and should have the same price as your second level. 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

4. 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.

5. Sign Quotes

Once the Hashflow server has received and validated your RFQ, 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.
It is important to note that the signature depends on the Chain. More specifically, the Chain ID. The currently supported Hashflow Chains (EVM-only), and their Chain IDs are as follows:
  • Ethereum: 1
  • BNB: 56
  • Polygon: 137
  • Avalanche: 43114
  • Arbitrum: 42161
  • Optimism: 10
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).

Examples

Now, let's look at some concrete examples that follow the steps described above. Suppose our signer is as follows (DO NOT USE the keys below in production):
  • Address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
  • Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Example 1 (Ethereum):
For the Quote data below:
{
pool: '0x5E4e65926BA27467555EB562121fac00D24E9dD2',
trader: '0x41b309236c87b1bc6fa8eb865833e44158fa991a',
effectiveTrader: '0xdf947F1E3031FE63Bd432f4E7b684DECe92C0bdA',
baseToken: '0x0000000000000000000000000000000000000000',
quoteToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
baseTokenAmount: '1000000000000000000',
quoteTokenAmount: '1500000000',
nonce: 10,
quoteExpiry: 1675809971216,
txid: '0x0267616e64616c6674686562726f776e67786d786e690014f8e119e4b0b80000'
}
Quote Hash: 0xa97136e16a2c0ef62635c74515b094725f72f2d6d13d0b11f5500de3d27f2659
EIP-191 Hash: 0x2e2c4dce7dc5c329c019d7fdb3b5756a36111776acc3319ccbc60ac250bd6d64
Signature: 0xea2ba7a7a8fc1c0ef93aec9833fde9f8bd425ddec45090cdd0c86a1bc75dd0f825e2a3e978d46a18f5ea9a61c09d52849d547c0f41ca6fae7480835307352a241b
Example 2 (BNB):
For the Quote data below:
{
pool: '0x22cbd249e6c68712dA6767f1077b28C87745FA6D',
eoa: '0xf959cC2dffA1a6328A16074dbe98f5eD1535ff9E',
trader: '0x41b309236c87b1bc6fa8eb865833e44158fa991a',
baseToken: '0x0000000000000000000000000000000000000000',
quoteToken: '0xe9e7cea3dedca5984780bafc599bd69add087d56',
baseTokenAmount: '1000000000000000000',
quoteTokenAmount: '300000000000000000000',
nonce: 10,
quoteExpiry: 1675809971216,
txid: '0x0267616e64616c6674686562726f776e67786d786e690014f8e119e4b0b80000'
}
Quote Hash: 0xecba311bc541362f075dc69d3187c32a3921a798c92a3efb2488b75d4d1cc4d0
EIP-191 Hash: 0x1f0eafb04949af2e72346bbacddeff5631b3b59e95674af97b4fa85035ccd75f
Signature: 0x35928adf32ee9a8e9720d8b3fb9a29131e7a347e45c89a4709760043d0824ba5326b29108faea1f9781ffe895a84afbe28b26752fadebf484b4784e795bdcd031b
While the hashes should always be the same for the same payload, signatures could be different. To test whether the generated signature is correct, we need to recover the signer address from the signature and the EIP-191 hash. The recovered address should match the 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 value mentioned above.
Example JS code on how to run address recovery:
import { utils } from 'ethers';
const recoveredAddress = utils.recoverAddress(
Buffer.from(eip191Hash.slice(2), 'hex'), // the slice eliminates the 0x prefix
Buffer.from(signature.slice(2), 'hex') // the slice elimintates the 0x prefix
);

6. 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.

7. (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.

8. 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 🥳

How to send errors

The 7 steps above covered the "happy path" – what happens when everything goes right. But the real world is messy. So, invariably, we'll encounter some error cases. The Hashflow WS API is designed in a way to easily support errors, as long as they are sent in a specific format.
Every message that is in response to a different message has the option of returning an error instead of the desired payload. To do so, you send a message with the following format:
{
"messageType": "<message>", // Same types as otherwise ("quote", "signature", etc)
"message": {
"error": "<failure>", // Only specific failures are recognized. See below.
"originalMessage": {
... // Original inbound message
}
}
}
Let's explain these some more:
  • messageType: The type of message you're sending. You treat this field is the exact same as you would for a successful request.
  • message.error: The error you want to return. We currently support the following error cases based on message type.
    • For all messages: 'invalid_input', 'internal_error', 'compliance', 'rate_limit'
      • When responds with 'rate_limit' in your error message, you can specify an "expiryTimestampMs" (UTC millisecond timestamp) that indicates until when this rate_limit would expire. We will then block any requests fall under given restrictions and not send messages to you until the timestamp has expired.
    • For quotes only: insufficient_liquidity', 'pair_not_supported', 'market_conditions'
    • For signatures only: 'incorrect_payload', 'payload_expired', 'market_conditions', 'signing_not_supported', 'invalid_signature'
  • message.originalMessage: The body of the original inbound message you received. We need this to be able to map the failure to a request. You should only send the body here (we know which messageType it was based on the type of your response).
To give an example, if you received an rfq message but you didn't want to give a quote because the trader had been blacklisted, you'd respond with
{
"messageType": "quote",
"message": {
"error": "compliance",
"expiryTimestampMs" : 1681590420000, // set this field when your error is rate_limit
"originalMessage": {
"rfqId": "0x67..",
"rfqType": 1,
"source": "hashflow",
...
}
}
}
as long as you respond in this format, we'll be able to parse these messages correctly.