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.

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. We support authenticating based on internal keys AND/OR by allowlisted IP address. Contact us (Telegram/Discord) to share how you'd like your market to be verified and what key and/or IP to check for.
  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. 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 programatically 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 can be either permissioned (only allowed addresses can contribute) or open. Keep in mind public pools are eligible for additional HFT tokens based on their performance (see HFT tokenomics).
After submitting the transaction, your new pool will be created. 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.
    EOA (externally owned account). 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 EOA trading is that you don't need to lock funds into a pool. The disadvantages are higher gas fees and it does NOT support native ETH.
  2. 2.
    Pool Funds. You can also use pool funds to market make. You can add funds to your pool using the Deposit function. Market making from hashflow will come out of the pool balance if not EOA is specified. The advantage of using pool funds is that they make trading cheaper which is an advantage on aggregators.
For public pools, you need to use (2) pool funds since EOA is not supported.

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 relays this to the specified market maker (or all market makers, if none is specified) as a WebSocket message with format:
{
"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,
}
}
After receiving this RFQ message, compute the quote you'd like to offer (based on market conditions, internal balances, etc) and – if you'd like to quote – return that quote 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 EOA (externally owned account), this should
// contain the wallet address of the EOA.
// The EOA 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,
// Set this to "0" for private pool / EOA trading.
"fees": string,
// The unix timestamp when the quote expires, in seconds.
"quoteExpiry": number,
}
}
If no marketMaker is specified, we request a quote from all connected MMs. The Hashflow server will wait 350 milliseconds for a quote and picks the best one (if there's multiple). If there are no quotes, we wait another 1 second (that's for production, it's 2 sec on staging and 4 sec on dev) for a response before timing out.

4. Support Signing Quotes

Once the Hashflow server has selected the best quote, we send a "signQuote" request to that quote's MM over the WebSocket to obtain the signature. The request is of format:
"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,
"fees": 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
}
}
}
The MM should then produce an Ethereum signature of the quote hash. The signature logic depends on whether you are market making through an EOA or pool funds.
See this example JavaScript for how to sign using an EOA:
const { utils } = require('ethers');
const { ecsign } = require('ethereumjs-util');
const quoteHash = return utils.solidityKeccak256(
[
'address',
'address',
'address',
'address',
'address',
'address',
'uint256',
'uint256',
'uint256',
'uint256',
'uint256',
'bytes32',
'uint256',
],
[
quoteData.pool,
quoteData.trader,
quoteData.effectiveTrader ?? quoteData.trader,
quoteData.eoa ?? ZERO_ADDRESS,
quoteData.baseToken,
quoteData.quoteToken,
quoteData.baseTokenAmount.toFixed(),
quoteData.quoteTokenAmount.toFixed(),
quoteData.fees.toFixed(),
quoteData.nonce,
quoteData.quoteExpiry,
quoteData.txid,
networkId,
]
);
);
const { v, r, s } = ecsign(this.hexToBuf(message), this.hexToBuf(signerPrivKey));
const signature = this.concatRSV(r, s, v);
The helper functions have been omitted above for simplicity. Again, you're not required to use javascript –– this is simply what our examples use.
NOTE: The public key (address) associated with the private key that is used for signing has to be used when creating the hashflow pool.
The MM should then broadcast the signature back through the WebSocket:
{
"messageType": "signature",
"message": {
"txid": string, // This is the quote txid previously sent.
"signature": string
}
}
The trader will then be able to take the signature and call Hashflows smart contract to execute the quote (within the expiry window).

5. Get your first signed RFQ.

Once you have implemented all the steps above and are connected, test your Market Maker by sending the following request to our staging API:
POST https://api-staging.hashflow.com/taker/v1/quote/signedRfq
Use these body params (if you're having issues, see the full request in Postman):
{
"networkId": 42, // 42 is Kovan, 1 is Mainnet
"source": "hashflow",
"baseToken": "0x07de306ff27a2b630b1141956844eb1552b956b5", // USDT (Kovan)
"quoteToken": "0xa0a5ad2296b38bd3e3eb59aaeaf1589e8d9a29a9", // WBTC (Kovan)
"trader": "0x2150DD462B496bd5e4A37b5411c13319427BE83E",
"baseTokenAmount": "1000000",
"marketMaker": "mmXYZ" // Obscured MM
}
If everything works correctly, you should receive a message from the WebSocket.
NOTE: The marketMaker field here is the "obscured" market maker. This is NOT the same field as the marketmaker you specify when connecting. We have an internal mapping so that we don't expose market maker identities. In this case, mmXYZ <> TestMM. Please ask us what your obscured MM name is.

6. (If using EOA) Set allowance for Private Pool

If using an EOA, set an allowance for the EOA to the Private Pool so that it can:
  • verify signatures
  • wrap / unwrap ETH
  • orchestrate the swaps
If you don't do this, your quotes will be rejected since we can't access the EOA funds.

7. Support Pairs

In order for us to know which token trading pairs are supported by each MM, our backend queries the supported token pairs via the WebSocket:
{
"messageType": "getPairs",
"message": {
"networkId": number // 1 for ETH L1
}
}
The MM should then respond with the following message:
{
"messageType": "pairs",
"message": {
"networkId": number,
"pairs": Array<{
"baseTokenName": string, // base token name (e.g. "ETH", "USDC")
"quoteTokenName": string, // quote token name (e.g. "ETH", "USDC")
}>
}
}
NOTE: Make sure to include both directions (e.g. {ETH, USDC}, {USDC, ETH}) if you support both directions.

8. Support Price Levels

In order to indicate current pricing for Hashflow and 1inch APIs, the MM should send price levels for each supported pair on every network, every second. 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. Price levels exclude fees.
Hashflow then caches these price levels for a max of 10 seconds (or until new levels are published) and returns them to 1inch when queried.

9. Execute your first Trade

Finally, test your new market maker end-to-end. You'll want to start by making a request to /signedRfq targeting your specific market maker. Make sure to use a test network (like Kovan) first to ensure everything works properly.
Once you have obtained a signed Quote, you can then execute the signed quote by calling the Hashflow router smart contract.
The contract has the following addresses:
  • Kovan (42): 0x46Bf7446748814E9A74CaE1284f80B8469388EB7
  • Mainnet (1): 0x79cdFd7Bc46D577b95ed92bcdc8abAba1844Af0c
and the following ABI:
{
"inputs": [
{
"components": [
{
"internalType": "enum IQuote.RFQType",
"name": "rfqType",
"type": "uint8"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "address",
"name": "eoa",
"type": "address"
},
{
"internalType": "address",
"name": "trader",
"type": "address"
},
{
"internalType": "address",
"name": "effectiveTrader",
"type": "address"
},
{
"internalType": "address",
"name": "baseToken",
"type": "address"
},
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "effectiveBaseTokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxBaseTokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxQuoteTokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "fees",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "quoteExpiry",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "txid",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "signedQuote",
"type": "bytes"
}
],
"internalType": "struct IQuote.Quote",
"name": "quote",
"type": "tuple"
}
],
"name": "tradeSingleHop",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
  • maxBaseTokenAmount / maxQuoteTokenAmount. These are what you receive in the API as baseTokenAmount / quoteTokenAmount. Sometimes you can receive a quote for higher than what you requested. It is essential that you use the requested amount in the effectiveBaseTokenAmount field.
  • effectiveBaseTokenAmount . The actual swapped amount. This has to be less than or equal to maxBaseTokenAmount. We suggest to keep them equal unless there's a discrepancy with the requested amount.
After submitting, go to Etherscan and confirm your trade went through.

10. Subscribe to Trades

Hashflow monitors all trades automatically in our backend. We offer the option for makers to subscribe to trades on their pools for faster updates.
This can be done by sending a subscribeToTrades message over the WebSocket
{
"messageType": "subscribeToTrades",
"message": {
"networkId": number, // network id of pool
"pool": string // pool id '0x...'
}
}
NOTE: The subscriptions expire on the backend, so make sure to send 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, 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.

Success!!

Congrats! You've successfully set up a market maker 🥳