Executor Guide

Building your own executor can be a profitable endeavor that also provides a public good. Below you will find a high-level guide to building your own executor.

Brink Order Execution

The role of an executor is to execute signed messages that have been created either through our API, brink.trade, or by a third party utilizing our smart contracts and network. When you go on the journey of building your own executor, you not only help to maintain the availability of automated order execution, but can also earn a profit while doing so.

Adapter Contracts

Building an Executor

Below you will find the high level steps you should take in order to build your own executor.

Load an Ethereum Wallet with Funds

In order to execute signed messages, you will need to have a wallet loaded with Ether. The amount is up to you, but this will be paying for the gas of the transactions you execute. Note that your executor may or may not take profit in tokens other than Ether depending on how you build it. If built to take profit in tokens, you will definitely want to monitor the balance of Ether in your executor wallet.

Setup your project

The team at Brink has built an SDK in order to make it easier to sign and execute orders. It is written in Node.js, and as such, we recommend starting off with a Node.js project base, our SDK, and Ethers. You will also want to install Brink Utils.

yarn add @brinkninja/sdk
yarn add @brinkninja/utils
yarn add ethers

Obtain and Store Messages

Once you have your project setup, you need to be able to retrieve the signed messages through our API, we also recommend storing them in a database of your choice and keeping track of their states, but this is not absolutely necessary and we publicize our own internally tracked states via our API. Our goal is to decentralize this order book in the future, but for now they are only available through our data store. The two APIs you will want to take note of are:

This API allows you to filter signed messages by our own internal state. We recommend you use this API to initialize your datastore and do not use this for continual polling which is provided by the next API.

GET https://api.brink.trade/messages?states=UNDER_LIMIT,UNDER_PROFIT

This API is a SSE get endpoint which will maintain a connection with our API server and continually notify new events as they are created. We recommend using this API to maintain the accuracy of your data store and to give your executor access to the most up-to-date signed messages.

GET https://api.brink.trade/events

An example below shows how to easily add event notifications to your Node.js application:

const EventSource = require("eventsource")
const source = new EventSource(`https://api.brink.trade/events`)
source.addEventListener('message', message => { console.log(message) })

Evaluate and Execute Message

The next steps once you have a datastore setup and new events are flowing in is to evaluate the profitability of messages, and optionally update their state in your datastore in order to filter out ones that are not executable or unlikely to be profitable. Once you have evaluated an order and have deemed it profitable, it is time to execute and make that sweet sweet profit.

Setup

Instantiate the Brink SDK and Ethers, you will need both to evaluate and execute messages. (Note you will need to setup your own Ethereum node or use a service like Infura to populate The ETH_NODE_RPC_URL field when instantiating the ethers provider. We also recommend you NEVER use your private key straight in your code, use a config solution instead)

const ethers = require('ethers)
const brinkSDK = require('@brinkninja/sdk')
const brinkUtils = require('@brinkninja/utils')

// Obtain a signer and a provider
const ethersProvider = await ethers.getDefaultProvider(ETH_NODE_RPC_URL)
const ethersSigner = await new ethers.Wallet(CONFIG.EXECUTOR_WALLET_PRIVATE_KEY)

// Instantiate with environment config
const brink = brinkSDK('dev')

Evaluation

You will not want to try and execute all signed messages that come into your system, many will fail and cost you gas. It is upon you to evaluate the profitability of each signed message and keep an updated state in order to have less failed transaction, and a higher profit output. Below are our recommendations for things to check when evaluating a signed message and helpful tools you can use. We recommend you think of your own and build you own adapter contracts in order to achieve the highest profits. Be creative!

Example Signed Message

"message": {
    "message": "0x4b4c4eeb5474afc041f3f966f125035fac103c59327d83093bdc6369721eb596",
    "signature": "0x210988d0b3842f3163604372c2b487500520cca44c89823234a0563ae549012841fea5b2f56f3ca1e33a5148b9d4078c5499a1399a6f158e0f868029f1a7c8ff1c",
    "signer": "0xea5dfd42f8d668d910478efab56c4f5c45375879",
    "accountAddress": "0xf43f82e8a0a30603681c855245264bdc11c6c655",
    "functionName": "metaDelegateCall",
    "signedParams": [
        {
            "name": "to",
            "type": "address",
            "value": "0xa96dc3076db393e83139c807227e53b54b007073"
        },
        {
            "name": "data",
            "type": "bytes",
            "value": "0xdc0ed0fe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000e7b9d7cd757a1a6838985c83c7f571346ee78e810000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000016d5ed6dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
            "callData": {
                "functionName": "ethToToken",
                "params": [
                    {
                        "name": "bitmapIndex",
                        "type": "uint256",
                        "value": "0"
                    },
                    {
                        "name": "bit",
                        "type": "uint256",
                        "value": "1"
                    },
                    {
                        "name": "token",
                        "type": "address",
                        "value": "0xe7b9d7cd757a1a6838985c83c7f571346ee78e81"
                    },
                    {
                        "name": "ethAmount",
                        "type": "uint256",
                        "value": "1000000000000000000"
                    },
                    {
                        "name": "tokenAmount",
                        "type": "uint256",
                        "value": "383118701"
                    },
                    {
                        "name": "expiryBlock",
                        "type": "uint256",
                        "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
                    },
                    {
                        "name": "to",
                        "type": "address"
                    },
                    {
                        "name": "data",
                        "type": "bytes"
                    }
                ]
            }
        }
    ]
}

1. Get an Instance of the Account

const account = brink.account(
    message.signer, 
    { provider: ethersProvider, signer: ethersSigner }
)

2. Check for expiration

Brink messages have an expiration block. If the expiration block is lower than the current block the message is expired then you should not execute the message, and you should also note this in your datastore.

expiryBlock (within callData in the above example message)

3. Check for Cancelled Message

Brink messages can be cancelled by the creator of the message by flipping a bit. You can check if it has been cancelled using our SDK. If the message is cancelled you should not execute the message, and you should also note this in your datastore.

(bitmapIndex and bit can be found in the callData params in the above example message)

const bitUsed = await account.bitUsed(message.bitmapIndex, message.bit)

4. Check the Account's Balance

Before executing a message, you should check the account balance of the address that signed the message. If they do not have the required token or ETH funds for the message, you should not execute the message, and you should also note this in your datastore.

In the example message, you can determine this is an ETH to token swap 
based on the functionName within the callData params
In this case, you should evaluate message.accountAddress's ETH funds

5. Check for History of Failures

At times we may not know why a message fails, but it can happen. If you have attempted to execute this message in the past and it failed, you should note that in your datastore so that your executor isn't continually attempting to execute a message with a history of failures.

6. Check for Profit

Finally the most difficult evaluation and where you can get the most creative is when you are checking the profitability of a signed message. This is where you can gain the biggest competitive edge against other executors in the network. We can't give you an exact formula for calculating profit, but here are some guidelines:

  • Check for the highest swap value using on chain data or an API. You will also need to create an adapter contract to interact with the chosen protocol. We provide our own Uniswap V3 adapter for public use, but we encourage you to build your own adapters and utilize AMMs other than Uniswap V3 to achieve the highest profit and best competitive edge.

  • Encode the function call to your adapter. You can use Brink Utils to help here.

const { encodeFunctionCall } = brinkUtils
const fnCall = encodeFunctionCall(
    'ethToToken', 
    [
        'address', 
        'uint256', 
        'address'
    ], 
    [
        message.tokenAddress, 
        message.tokenAmount, 
        message.accountAddress
    ]
)
  • Calculate the cost of gas to execute this transaction and subtract it from excessEth. You can use our SDK's estimateGas feature to help estimate the cost of the transaction.

const gasCostObj = await this.account.estimateGas.sendLimitSwap(
    signedMessage, 
    adapter.address, 
    fnCall
)
const gasCost = gasCostObj.gas
const profit = excessEth - gas

7. Execute the Transaction!

Once you have calculated the profit, you can execute the signed message and reap the rewards!

const tx = await this.account.sendLimitSwap(
    signedMessage, 
    adapter, 
    fnCall
)

Last updated