Best practices
This page describes some of the best practices observed for bot development.
Target specific conditions
Write your bot to target a specific condition so that it does not generate findings for every other block/transaction. Verbose bots can make it hard to distinguish the signal from the noise i.e. if a bot alerts on more than 5% of transactions, the usefulness of those alerts would not be very high as it is difficult to know what to pay attention to.
Break down large bots into smaller files
Your bot may be looking for multiple conditions that you could write in a single file. We recommend keeping each condition in its own file. This will make testing your bot easier and keep the code more maintainable. You would then combine all the bots in the top-level entrypoint file (i.e. agent.js). See here for an example.
Keep findings lean
There is a metadata
field in the Finding
object that you can use to store any extra information that is useful. Try to keep the data here as lean as possible i.e. don't throw the whole TransactionEvent
into the metadata since that information is already available on Etherscan.
Create useful alertIds
You are required to populate the alertId
field of the Finding
object. Ideally, you would want it to be unique so that when you search for your alertId
in Forta App it only shows your bot's alerts. Typically, an alertId
has a string component (describing either the protocol or project) and a numeric component (to distinguish between different types of alerts about the same protocol or project) e.g. TETHER-1
. It is left to the bot developer to choose what makes sense for their bot.
Write unit tests
You should write and maintain unit tests for your bot. This will ensure a high quality bar and also allow you to test all edge cases in your bot. Include both negative (i.e. when alerts should not be created) and positive (i.e. when alerts should be created) test cases for completeness.
When writing tests that involve log events, you can mock out the filterLog
SDK method instead of having to fiddle around with event topics and signatures. See here for an example. You can similarly mock out the filterFunction
SDK method when writing tests that involve function calls. See here for an example.
Conduct code reviews
It is strongly recommended to conduct code reviews within your team. This will help ensure that any bugs are identified and all edge cases are covered by your bot. See the code review checklist for how to conduct a comprehensive review.
Include documentation
Ensure that your project documentation README.md is complete, clear and concise. Briefly describe what your bot does, as well as each type of alert it can produce under which conditions. You should also include real test data that someone could use to verify the bot's behaviour. See the example README.md included with the starter projects for an example.
Use the initialize handler
Your bot may need to do some asynchronous initialization when it starts, for example, by fetching data from some external API. You should use the initialize
handler function for such logic.
Limit number of network calls
Your bot may need to make network calls to fetch data from external sources e.g. token prices. Be sure to make only the necessary network calls in order to respond in a timely manner. Another useful strategy for this could be to use caching. Also, if querying lots of on-chain data (e.g. token balances for a list of accounts), consider using the ethers-multicall
package listed in the Useful libraries section to fetch all the data in a single http request.
Use caching where possible
Caching is a great way to improve performance. If you need to store the result of a network call or some other calculation, try to use an in-memory cache. The lru-cache
package listed in the Useful libraries section is a great option.
Use concurrency where possible
Try to make use of concurrency to maximize performance. For example, if you are firing multiple http requests to fetch on-chain data, you can use the ethers-multicall
package to fetch all the data in a single http request. Also, if firing multiple network calls, you can fire all the requests at the same time using something like Promise.all
in Javascript.
Beware of case-sensitivity
When comparing addresses in your code, be mindful of case-sensitivity. The SDK will return addresses in the BlockEvent
and TransactionEvent
as lowercase, but if you are comparing to a checksum address it will not be equal.