12 min read
Overview
Dealing with real-time data on DeFi can be challenging and time-consuming. However, QuickNode's Streams and Functions products provide a powerful solution to automate and streamline this process. In this guide, we'll walk you through setting up a Twitter (X) bot that tracks new liquidity pools on Uniswap V2 and V3 using QuickNode's Streams and Functions. This bot will capture event data, filter it to identify new liquidity pools, and post updates to Twitter (X) via a JavaScript/TypeScript library.
By leveraging QuickNode's serverless infrastructure, there is no need for any server-side code or maintain a database. This allows for a more efficient and cost-effective solution.
Although we use Base Mainnet in this tutorial, the same approach can be applied to other blockchain networks supported by QuickNode. Just ensure you modify the filtering function to match the specific events and contract addresses of your desired chain.
What You Will Do
- Configure QuickNode’s Streams to detect new liquidity pools on Uniswap V2/V3 using its filtering capabilities
- Set up a serverless function using QuickNode’s Functions, which natively supports the Twitter library in its Node.js runtime, to process the data and post updates directly to Twitter (X)
- Create an automated, real-time Twitter bot to share insights on new liquidity pools on the Base mainnet
What You Will Need
- A QuickNode account
- Twitter (X) Developer account to create the bot and access Twitter’s API
- JavaScript/TypeScript knowledge
- Basic understanding of event logs
Streams and Functions
In blockchain development, handling real-time data and automating responses can be complex and resource-intensive. QuickNode’s Streams and Functions products are here to simplify this process, empowering developers to focus on building great applications without worrying about managing servers or processing irrelevant data.
Streams
Streams is a powerful blockchain data streaming and ETL (Extract, Transform, Load) service that makes real-time and historical blockchain data easily accessible. Instead of relying on constant polling or complex infrastructure, Streams provides an efficient way to receive live blockchain data or backfill historical data
Key Features of Streams
- Real-Time Data Streaming: Instantly receive live blockchain data without the need for constant polling.
- Historical Data Backfilling: Retrieve historical blockchain data with upfront cost and completion estimates.
- Customizable Filters: Apply JavaScript-based rules to tailor the data you receive, tracking events, decoding data, and more.
- Cost-Efficient: Priced by data volume, Streams keeps costs predictable.
- Data Consistency: Handles chain reorganizations seamlessly, ensuring no data loss.
- Compression: Minimize bandwidth usage with optimized data transmission.
- Functions Integration: Enhance Streams with serverless processing to unlock even more powerful workflows.
Functions
Functions provide a serverless platform that allows you to filter, transform, and enhance blockchain data on the fly, without maintaining servers or infrastructure. They allow you to process real-time data, integrate external APIs, and create serverless APIs that scale automatically.
Key Features of Functions
- Serverless Flexibility: Write, deploy, and run code in JavaScript/TypeScript without worrying about infrastructure.
- External Library Support: Use popular libraries like
ethers.js
andtwitter-api-v2
directly within your functions. - API-Ready: Each Function is automatically exposed as an API, making integration seamless.
- Key-Value Store: Store and retrieve data efficiently, perfect for managing extensive lists of data points, such as wallet addresses, caching token names and symbols.
To easy getting familiar with Functions, we've created a Functions Library that contains a collection of commonly used functions. You can find the library here.
How We Use Streams and Functions in This Guide
Streams for Real-Time Data
We set up a Stream to monitor new liquidity pools on Uniswap V2 and V3. Using its filtering capabilities, only relevant events are sent to a destination, reducing unnecessary data handling.
For example, the following is a sample of the data sent to the Function, instead of a bulk of data:
[
{
"poolAddress": "0x0000000000000000000000000000000000000001",
"token0": "0x4200000000000000000000000000000000000006",
"token1": "0xce15615dec6758825849e8d093c695e86ec0f8e0",
"txHash": "0xc90fce8ede27f1840e2f50b7ce68b3a74a80b20136167e49359a50d3f4a7f09a",
"type": "Uniswap V3"
}
]
Functions for Processing and Automation
The Stream’s filtered data is sent to a Function, where we:
- Parse and enrich the data, such as retrieving token names and symbols using
ethers.js
, which is natively included in the Functions Node.js runtime - Store token names and symbols in a Key-Value Store to avoid redundant blockchain calls
- Format the data into actionable insights (e.g., crafting a message for Twitter)
- Automatically post updates to Twitter (X) via the Twitter API
Tracking New Liquidity Pools on Uniswap V2 and V3 using Streams
In this section, we will create a Stream to monitor new liquidity pools on Uniswap V2 and V3 and set up a filtering function to process the events. This function will extract relevant data, including token addresses, pool addresses, and transaction details, preparing it for further use in our application.
Step 1: Create a Stream
Let's start by creating a Stream to monitor new liquidity pools on Uniswap V2 and V3.
- Log in to your QuickNode dashboard and navigate to the Streams section.
- Click Create New Stream.
- Choose Base Mainnet as the network.
- Select the dataset Receipts to capture detailed event logs from transactions.
- Leave Stream start and Stream end as default values, Latest block and Doesn't end, since we start tracking from now and continue tracking.
- Click Modify the payload before streaming to filter the raw data.
- Add a custom filter to narrow down the data to only relevant liquidity pool creation events:
- Monitors events emitted by the Uniswap V2 and V3 Factory contracts.
- Target specific event topics, such as
PairCreated
for Uniswap V2 andPoolCreated
for Uniswap V3.
Here’s the JavaScript filter you’ll use:
Click to expand
function main(stream) {
// Define constants for filtering
const uniswapV3PoolCreatedTopicHash =
'0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118' // Uniswap V3 PoolCreated event topic
const uniswapV2PairCreatedTopicHash =
'0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9' // Uniswap V2 PairCreated event topic
const uniswapV3FactoryAddress = '0x33128a8fC17869897dcE68Ed026d694621f6FDfD' // Uniswap V3 Factory address
const uniswapV2FactoryAddress = '0x8909Dc15e40173Ff4699343b6eB8132c65e18eC6' // Uniswap V2 Factory address
// Initialize pools array
const pools = []
const data = stream[0]
// Loop through each transaction receipt in the stream
data.forEach(receipt => {
// Filter for Uniswap V3 PoolCreated events
const newV3Pools = receipt.logs.filter(
log =>
log.address.toLowerCase() === uniswapV3FactoryAddress.toLowerCase() &&
log.topics[0] === uniswapV3PoolCreatedTopicHash
)
// Filter for Uniswap V2 PairCreated events
const newV2Pools = receipt.logs.filter(
log =>
log.address.toLowerCase() === uniswapV2FactoryAddress.toLowerCase() &&
log.topics[0] === uniswapV2PairCreatedTopicHash
)
// Process Uniswap V3 pool creation events
newV3Pools.forEach(poolEvent => {
const token0 = '0x' + poolEvent.topics[1].slice(26) // Token0 address
const token1 = '0x' + poolEvent.topics[2].slice(26) // Token1 address
const poolAddress = '0x' + poolEvent.data.slice(-40) // New pool address
// Push Uniswap V3 pool creation object
pools.push({
type: 'Uniswap V3',
token0,
token1,
poolAddress,
txHash: receipt.transactionHash,
timestamp: receipt.timestamp, // Assuming receipt contains a timestamp field
})
})
// Process Uniswap V2 pair creation events
newV2Pools.forEach(pairEvent => {
const token0 = '0x' + pairEvent.topics[1].slice(26) // Token0 address
const token1 = '0x' + pairEvent.topics[2].slice(26) // Token1 address
const poolAddress = '0x' + pairEvent.data.slice(26, 66) // New pair address
// Push Uniswap V2 pool creation object
pools.push({
type: 'Uniswap V2',
token0,
token1,
poolAddress,
txHash: receipt.transactionHash,
timestamp: receipt.timestamp,
})
})
})
return pools.length > 0 ? pools : null
}
- Type a test block number in the Block Number (i.e.,
22537210
) field and click Run. You should see a list of Uniswap V3 and V2 pool creation events in the output like the following:
[
{
"poolAddress": "0x0000000000000000000000000000000000000001",
"token0": "0x4200000000000000000000000000000000000006",
"token1": "0xce15615dec6758825849e8d093c695e86ec0f8e0",
"txHash": "0xc90fce8ede27f1840e2f50b7ce68b3a74a80b20136167e49359a50d3f4a7f09a",
"type": "Uniswap V3"
}
]
- Click Next to proceed to the next step.
Step 2: Select a Destination
After setting up the Stream, you’ll be prompted to select a destination.
- Choose Create a New Function from the available options. This will allow us to process the filtered data in a serverless Function.
- Name your Function (e.g., “Uniswap Pool Processor”).
- Select
Node.js 20
as the runtime. - Proceed to set up the logic for handling data in the Function (detailed in the next step).
Step 3: Create a Function
This Function automates the process of posting tweets about new liquidity pools detected by QuickNode Streams.
- It starts by checking the Key-Value Store for cached token metadata (name and symbol). If the data isn’t cached, it fetches it using the
ethers.js
library and stores it in the Key-Value Store for future use. If the Key-Value Store would not be available, the Function would call the blockchain directly each time to fetch token metadata. - Once the metadata is enriched, the Function formats the data into a tweet message, highlighting details like token names, symbols, and pool address.
- Using the
twitter-api-v2
library, it posts these tweets, returning the pool address and transaction hash or any errors encountered.
Here’s the JavaScript Function you’ll use:
Click to expand
const { TwitterApi } = require('twitter-api-v2')
const { ethers } = require('ethers')
// Twitter API credentials
const client = new TwitterApi({
appKey: 'YOUR_APP_KEY',
appSecret: 'YOUR_APP_SECRET',
accessToken: 'YOUR_ACCESS_TOKEN',
accessSecret: 'YOUR_ACCESS_SECRET',
})
const twitterClient = client.readWrite
// Blockchain provider setup (e.g., QuickNode RPC)
const provider = new ethers.JsonRpcProvider('YOUR_QUICKNODE_ENDPOINT')
// ERC-20 ABI for fetching token metadata
const erc20Abi = [
'function name() view returns (string)',
'function symbol() view returns (string)',
]
/**
* Fetches token details (name and symbol) from a blockchain or Key-Value Store.
* @param {string} tokenAddress - Address of the ERC-20 token.
* @returns {Promise<Object>} - Token details with name and symbol.
*/
async function getTokenDetails(tokenAddress) {
try {
// Check Key-Value Store for cached token metadata
const cachedData = await qnLib.qnGetSet(tokenAddress)
if (cachedData) {
console.log(`Cache hit for token: ${tokenAddress}`)
return JSON.parse(cachedData) // Return cached data
}
// Cache miss: Fetch token details from blockchain
const contract = new ethers.Contract(tokenAddress, erc20Abi, provider)
const name = await contract.name()
const symbol = await contract.symbol()
// Cache the fetched data
await qnLib.qnAddSet(tokenAddress, JSON.stringify({ name, symbol }))
console.log(`Cached data for token: ${tokenAddress}`)
return { name, symbol }
} catch (error) {
console.error(
`Error fetching details for token ${tokenAddress}:`,
error.message
)
return { name: null, symbol: null }
}
}
/**
* Creates a tweet message for a new liquidity pool.
* @param {Object} pool - Data for the new liquidity pool.
* @returns {string} - Formatted tweet message.
*/
function createTweet(pool) {
return `
LP Tracker Bot, by QuickNode's Streams and Functions
🚀 New ${pool.type} Liquidity Pool Created on Base!
🔹 Token 0: ${pool.token0Symbol || pool.token0} (${
pool.token0Name || 'Unknown'
})
🔹 Token 1: ${pool.token1Symbol || pool.token1} (${
pool.token1Name || 'Unknown'
})
Pool Address: ${pool.poolAddress}
`
}
/**
* Posts tweets for new liquidity pools.
* @param {Array} pools - Array of pool data.
* @returns {Promise<Array>} - Array of tweet IDs with related pool information.
*/
async function postToTwitter(pools) {
const results = []
for (const pool of pools) {
const message = createTweet(pool)
try {
const createdTweet = await twitterClient.v2.tweet(message)
results.push({
tweetId: createdTweet.id_str,
poolAddress: pool.poolAddress,
txHash: pool.txHash,
})
} catch (error) {
results.push({
error: `Failed to post tweet for pool ${pool.poolAddress}`,
message: error.message,
})
}
}
return results
}
/**
* Main function to process stream data and post to Twitter.
* @param {Object} params - Parameters passed to the function.
* @returns {Promise<Object>} - Result of the operation.
*/
async function main(params) {
console.log('Processing stream data...')
try {
const { data } = params
// Validate input
if (!Array.isArray(data) || data.length === 0) {
return {
message: 'No data to process',
result: 'No tweets sent.',
}
}
// Enrich data with token names and symbols
for (const pool of data) {
const token0Details = await getTokenDetails(pool.token0)
const token1Details = await getTokenDetails(pool.token1)
pool.token0Name = token0Details.name
pool.token0Symbol = token0Details.symbol
pool.token1Name = token1Details.name
pool.token1Symbol = token1Details.symbol
}
// Post to Twitter
const tweetResults = await postToTwitter(data)
return {
message: 'Streams data sent to Twitter',
processedPools: data.length,
tweetResults,
}
} catch (error) {
console.error('Error in main function:', error.message)
return {
error: 'Failed to send data to Twitter',
details: error.message,
}
}
}
// Export the main function for use in QuickNode
module.exports = { main }
Step 4: Set Up Twitter (X) API Keys and QuickNode Endpoint
Before running this Function, ensure you have the necessary credentials and URLs configured:
Twitter API Keys
- Go to the Twitter Developer Portal, create an app, and generate the following credentials:
- API Key and API Secret Key
- Access Token and Access Token Secret
- Replace
YOUR_APP_KEY
,YOUR_APP_SECRET
,YOUR_ACCESS_TOKEN
, andYOUR_ACCESS_SECRET
in the Function code with the corresponding credentials. - In your app's settings, enable User Authentication Settings and set permissions to Read & Write since this Function will be posting tweets.
- If prompted for a Callback URI or Redirect URL, you can use
https://example.com
. This is only relevant for apps requiring user login and can be safely ignored in this context.
QuickNode Endpoint URL
- Sign up for QuickNode, if you haven't already.
- Create a new endpoint for the desired blockchain network (e.g., Base mainnet) on the QuickNode dashboard.
- Replace
YOUR_QUICKNODE_ENDPOINT
in the Function code with the QuickNode endpoint URL.
Step 5: Test the Function
Select a proper test block number to test the Function (i.e., 22537210
), and click Test Function. You should see an output similar to the following, if the Function is working correctly:
Results:
{
"message": "Streams data sent to Twitter",
"processedPools": 1,
"tweetResults": [
{
"poolAddress": "0xa07aaa70793649db7a80f722491ff6d707b41dcb",
"txHash": "0xc90fce8ede27f1840e2f50b7ce68b3a74a80b20136167e49359a50d3f4a7f09a"
}
]
}
Then, you can check the tweets posted to Twitter (X) to verify that the Function is working as expected.
Step 6: Deploy the Function
Once you're satisfied with the Function's functionality, you can deploy it to QuickNode by clicking Create a Stream. This will create a new Stream and deploy the Function to it.
What’s Next?
Now that you’ve successfully set up a Function to track and tweet about new liquidity pools, here are some ideas to enhance your bot:
Add More Chains Expand your app to monitor liquidity pool creation across multiple blockchains like Ethereum, Polygon, or Binance Smart Chain. Create a new Stream for each chain and deploy the Function to it.
Enrich Tweets with Analytics Enhance your tweets by adding analytics like initial liquidity amounts, trading fees, or previous pool initializations of the deployer address.
Integrate More Destinations Send updates to additional platforms like Discord or Telegram using webhooks.
Automate Alerts for Specific Tokens Set up conditional alerts for specific tokens or pairs. For example, notify only when pools include a popular token like USDC or DAI.
Sky is the limit, building on top of QuickNode's Streams and Functions is a great way to extend your app's functionality and reach a wider audience. Explore the QuickNode documentation and resources to learn more about how to customize your app and integrate it with other platforms.
Conclusion
By following this guide, you've set up an automated Twitter (X) bot that captures real-time data on new liquidity pools on Base using QuickNode’s Streams and Functions. This bot can provide timely alerts on DeFi activity and can be extended further to track additional events or metrics as needed.
If you are stuck or have questions, drop them in our Discord. Stay up to date with the latest by following us on Twitter (@QuickNode) or our Telegram announcement channel.
Additional Resources
- QuickNode Streams Documentation
- QuickNode Functions Documentation
- Key-Value Store Documentation
- Streams Guides
- Functions Guides
- X API Documentation
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.