BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)
The code snippet below includes all the imports for the `executeTrade.js` file and setup wallet and provider for trade execution.
```js
dotenv.config();
const RPC_URL = "https://bsc-dataseed.binance.org/";
const provider = new JsonRpcProvider(RPC_URL);
const privateKey = process.env.PRIVATE_KEY1;
const wallet = new Wallet(privateKey, provider);
```
## Setting Up the Kafka Consumer to Listen to Bitquery Streams
In this section we will setup a Kafka consumer in JS using the `kafkajs` library. Extended tutorials for other languages are available above.
### Imports
```js
const { CompressionTypes, CompressionCodecs } = pkg;
CompressionCodecs[CompressionTypes.LZ4] = new LZ4().codec;
```
### Defining Constants
```js
const username = process.env.KAFKA_USERNAME;
const password = process.env.KAFKA_PASSWORD;
const topic = "bsc.tokens.proto";
const id = uuidv4();
```
### Initialising Kafka Consumer
```js
const kafka = new Kafka({
clientId: username,
brokers: [
"rpk0.bitquery.io:9092",
"rpk1.bitquery.io:9092",
"rpk2.bitquery.io:9092",
],
sasl: {
mechanism: "scram-sha-512",
username: username,
password: password,
},
});
const consumer = kafka.consumer({ groupId: username + "-" + id });
```
### Creating `convertBytes` Helper Function
Since the on-chain data is encoded in hex, we will need a convert function to get them into human readable format.
```js
const convertBytes = (value, encoding = "hex") => {
if (encoding === "base58") {
return bs58.default.encode(value);
}
return value?.toString("hex");
};
```
### Send Transaction with Nonce Functionality
The main purpose of this function is to make the overall trade execution of the bot less prone to errors due to transaction failure/revert. This would avoid scenarios where a token might be purchased but not sold due to transaction failure, which could lead to losses.
1. Defining Variable and Nonce Initiator
```js
let nextNonce;
async function initNonce() {
const current = await provider.getTransactionCount(wallet.address, "pending");
nextNonce = BigInt(current);
}
```
2. Creating the `sendTxWithNonce` Function
```js
async function sendTxWithNonce(txRequest, maxRetries = 3) {
if (typeof nextNonce === "undefined") {
// If nonce not initialized yet, wait briefly
await new Promise((r) => setTimeout(r, 500));
}
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// Attach the current nonce
txRequest.nonce = Number(nextNonce);
// Slot in a default gasPrice if not explicitly set:
if (!txRequest.gasPrice) {
const base = (await provider.getFeeData()).gasPrice;
txRequest.gasPrice = base;
}
const txResponse = await wallet.sendTransaction(txRequest);
nextNonce++; // only bump after successful send
return await txResponse.wait();
} catch (err) {
const msg = (err.reason || err.message || "").toLowerCase();
if (msg.includes("replacement underpriced") && attempt < maxRetries) {
// bump gasPrice by ~10% and retry
const bumped = (BigInt(txRequest.gasPrice) * 110n) / 100n;
txRequest.gasPrice = bumped;
console.warn(
`⚠️ Replacement underpriced—bumping gas to ${bumped.toString()} and retrying (${
attempt + 1
}/${maxRetries})`
);
continue;
}
throw err;
}
}
throw new Error("Exceeded maxRetries for transaction");
}
```
## Buy Token Functionality via Launchpad
In this section we will how to automate the buy functionality using etherjs.
The code snippets given below are written in `executeTrade.js` file, which is in the same directory as the `index.js`(entrypoint) file.
1. Defining Constants
```js
// Launchpad contract (for both buy and sell)
const LAUNCHPAD_ADDRESS = ethers.getAddress(
"0x5c952063c7fc8610ffdb798152d69f0b9550762b"
);
const BUY_ABI = [
{
inputs: [
{ internalType: "address", name: "token", type: "address" },
{ internalType: "address", name: "to", type: "address" },
{ internalType: "uint256", name: "funds", type: "uint256" },
{ internalType: "uint256", name: "minAmount", type: "uint256" },
],
name: "buyTokenAMAP",
outputs: [],
stateMutability: "payable",
type: "function",
},
];
const buyContract = new ethers.Contract(LAUNCHPAD_ADDRESS, BUY_ABI, wallet);
```
2. Creating the Buy Function
```js
export async function buyViaLaunchpad(
tokenAddress,
amountBNB = "0.001",
minAmount = 0n
) {
await initNonce();
try {
const funds = parseEther(amountBNB);
console.log(`🛒 Buying token ${tokenAddress} with ${amountBNB} BNB…`);
const data = buyContract.interface.encodeFunctionData("buyTokenAMAP", [
tokenAddress,
wallet.address,
funds,
minAmount,
]);
const txRequest = {
to: LAUNCHPAD_ADDRESS,
data,
value: funds,
gasLimit: 300_000,
};
const receipt = await sendTxWithNonce(txRequest);
console.log("✅ Purchase TX mined in block", receipt.blockNumber);
return receipt;
} catch (err) {
console.error("❌ Purchase failed:", err.reason || err.message || err);
return null;
}
}
```
## Sell Token Functionality via Launchpad
1. Defining Constants
```js
const SELL_ABI = [
{
constant: false,
inputs: [
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
name: "sellToken",
outputs: [],
payable: false,
type: "function",
},
];
const ERC20_ABI = [
"function approve(address spender, uint256 amount) external returns (bool)",
"function decimals() view returns (uint8)",
"function balanceOf(address account) view returns (uint256)",
];
const sellContract = new ethers.Contract(LAUNCHPAD_ADDRESS, SELL_ABI, wallet);
```
2. Creating the Sell Token Function
```js
export async function sellTokenViaLaunchpad(tokenAddress) {
await initNonce();
try {
const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, wallet);
// 1) fetch decimals + balance
const [decimals, rawBalance] = await Promise.all([
tokenContract.decimals(),
tokenContract.balanceOf(wallet.address),
]);
if (rawBalance === 0n) {
console.log(`⚠️ Balance is zero for ${tokenAddress}, skipping sell.`);
return null;
}
console.log(`🔓 Approving ${rawBalance.toString()} tokens for sale…`);
const approveData = tokenContract.interface.encodeFunctionData("approve", [
LAUNCHPAD_ADDRESS,
rawBalance,
]);
await sendTxWithNonce({
to: tokenAddress,
data: approveData,
gasLimit: 100_000,
});
console.log(`💰 Selling ${rawBalance.toString()} of ${tokenAddress}…`);
const sellData = sellContract.interface.encodeFunctionData("sellToken", [
tokenAddress,
rawBalance,
]);
const txRequest = {
to: LAUNCHPAD_ADDRESS,
data: sellData,
gasLimit: 300_000,
};
const receipt = await sendTxWithNonce(txRequest);
console.log("✅ Sell TX mined in block", receipt.blockNumber);
return receipt;
} catch (err) {
console.error("❌ Sale failed:", err.reason || err.message || err);
return null;
}
}
```
## Filtering for Four Meme Token Creation in the `run` Function
This is the main function for this script, where the kafka consumer instance is ran, newly created Four Meme tokens are checked and trades are executed. All the code snippets under this sub-section are written under the `run` function.
```js
const run = async () => {
....
}
```
1. Defining Variables
```js
let ParsedMessage = await loadProto(topic); // Load proto before starting Kafka
await consumer.connect();
await consumer.subscribe({ topic, fromBeginning: false });
const seenTokens = new Set(); // Track processed token addresses
```
2. Running the Consumer Instance
In this section, we use the protobuf schema from the Bitquery library, which is available [here](https://github.com/bitquery/streaming_protobuf). For JS, the schema is available as a package [here](https://www.npmjs.com/package/bitquery-protobuf-schema).
We're listening to `bsc.tokens.proto` topic, we're using the JS library to match the output to the schema. For the `bsc.tokens.proto` topic, Bitquery uses the [TokenBlockMessage](https://github.com/bitquery/streaming_protobuf/blob/930e4a8c2d69de2d6da11a4d31ee5f4e4b11c5fb/evm/token_block_message.proto#L41) schema which contains the `TokenTransfer` message format. This structure allows us to parse out key data such as token addresses, transaction senders, and smart contract information.
We're using Four Meme DEX address `0x5c952063c7fc8610ffdb798152d69f0b9550762b` to track token creation.
```js
await consumer.run({
autoCommit: false,
eachMessage: async ({ message }) => {
try {
// Getting Transfers
const buffer = message.value;
const decoded = ParsedMessage.decode(buffer);
const msgObj = ParsedMessage.toObject(decoded, { bytes: Buffer });
const transfers = msgObj.Transfers;
// Iterating Transfers
for (let i in transfers) {
const transfer = transfers[i];
const to = `0x${convertBytes(transfer.TransactionHeader.To)}`;
const sender = `0x${convertBytes(transfer.Sender)}`;
const tokenAddr = `0x${convertBytes(transfer.Currency.SmartContract)}`;
const name = transfer.Currency.Name;
const symbol = transfer.Currency.Symbol;
// Checking the conditions implying Four Meme Token Creation using the DEX Address
if (
to == "0x5c952063c7fc8610ffdb798152d69f0b9550762b" &&
sender == "0x0000000000000000000000000000000000000000"
) {
// Checking if the token is already seen and updating the list of seen tokens if the token is new
if (!seenTokens.has(tokenAddr)) {
seenTokens.add(tokenAddr);
console.log(`🚀 New Token: ${tokenAddr}, ${name} (${symbol})`);
// Buying the token
const buyReceipt = await buyViaLaunchpad(tokenAddr, "0.001", 0n);
if (buyReceipt) {
// Schedule Sell in 60 seconds
setTimeout(async () => {
try {
await sellTokenViaLaunchpad(tokenAddr);
} catch (e) {
console.error(
`❌ Sell error for ${tokenAddr}:`,
e.reason || e.message || e
);
}
}, 60 * 1000);
}
}
}
}
} catch (err) {
console.error("Error decoding Protobuf message:", err);
}
},
});
```
### Creating the Entrypoint for the script
```js
run().catch(console.error);
```
## Running the Script
To run the bot script locally, enter the following command
```sh
node index.js
```
You can also, run this script on cloud platforms by getting a VM(virtual machine instance). Once you have a VM with **Node**, **Git** and **NPM** installed, you can get the code on the same VM using these commands.
```sh
git clone < repository link >
cd < folder name >
sudo npm install -g pm2
pm2 start index.js --name "evm-sniper"
```
The status and logs of the bot could be checked using these commands
```
pm2 status
pm2 logs evm-sniper
```
## Important Notes
- Test carefully! On-chain transactions are irreversible.
- Gas fees and front-running risks exist on BSC—proceed at your own risk.
- Code is for educational and research purposes only.
## Video Tutorial
This stream has pre-calculated OHLC data in the response which you can feed directly to your charting solution without additional calculations. Our **cryptocurrency price api** is accessible through GraphQL queries and subscriptions, making it easy to integrate into your applications. The **crypto prices api** supports **real-time streaming** and **recent OHLC history** (**rolling 30 days**); for **older** OHLC, use **`DEXTradeByTokens`** as in the [OHLC guide](/docs/trading/crypto-price-api/crypto-ohlc-candle-k-line-api/#crypto-price-api-vs-dextradebytoken).
> **Note**: All queries can be converted to a GraphQL stream (WebSocket) by changing the keyword `query` to `subscription`
## Kafka Topic for Crypto Price Stream: `trading.prices`
This Kafka topic delivers **real-time, pre-aggregated price data** for tokens, currencies, and trading pairs across all supported blockchains. The data structure is a combination of all 3 cubes described in next section.
Schema for the proto topic is [here](https://github.com/bitquery/streaming_protobuf/tree/main/market).
The [python package](https://pypi.org/project/bitquery-pb2-kafka-package/) and [npm package](https://www.npmjs.com/package/bitquery-protobuf-schema) already have all schema updated.
Each message contains:
- **Price metrics** – OHLC (Open, High, Low, Close), Mean Price, SMA, WMA, EMA in USD
- **Volume data** – Base, Quote, and Base in USD
- **Supply data** – currency-level MarketCap, FullyDilutedValuationUsd, CirculatingSupply, TotalSupply, and MaxSupply ([Supply fields reference](/docs/trading/crypto-price-api/supply-fields))
- **Interval-based aggregation** – fixed durations (1s, 3s, 5s, 10s, etc.)
- **Clean feed** – low-quality and outlier trades are filtered automatically for accuracy as per the price algorithm discussed above.
> **Note on `"Quotes"`**:
> The `"Quotes": [...]` section in the Kafka stream shows how much was traded in the native token (e.g., SOL) and how the USD value was derived.
> All following price entries (`MeanPrice`, `SMA`, `OHLC`, etc.) are already expressed in USD and ready for direct use in charting, bots, or analytics.
## Cubes in the API
The Price APIs have three core data cubes:
- **Tokens**: One blended price per token per chain, aggregated across every pool the token trades in. Use it for chain-wide streams and per-chain aggregates.
- **Currencies**: An aggregated view of tokens that represent the same underlying asset. For example, tokens like cbBTC, WBTC, and other Bitcoin-wrapped tokens are all grouped under the Bitcoin currency.
- **Pairs**: Price and volume data for token pairs on specific markets/protocols. E.g., SOL/USDC on Raydium (Solana) or ETH/USDT on Uniswap (Ethereum). **This is also the recommended cube for a specific token's price** — filter to its top market with [`Ranking: { Position: { eq: 1 } }`](/docs/trading/crypto-price-api/pairs#most-accurate-token-price).
> Note: Expressions are supported in this API.
### Currencies
Currencies are representation of all tokens on various chains supported in Crypto Price API(Price Index). For example, take the case of Bitcoin, while it is a native token on Bitcoin chain, it is also traded on EVM chains as WBTC ( wrapped BTC). Now all these representations of BTC are represented as a single currency.
> Note: We include `Volume: {Usd: {gt: 5}}` to further remove extreme outliers; the stream already pre-filters outliers—this is an additional check.
```graphql
{
Trading {
Currencies(
limit: { count: 1 }
orderBy: { descending: Block_Time }
where: {
Currency: { Id: { is: "bid:bitcoin" } }
Volume: { Usd: { gt: 5 } }
}
) {
Volume {
Usd
Quote
BaseAttributedToUsd
Base
}
Supply {
MarketCap
FullyDilutedValuationUsd
CirculatingSupply
TotalSupply
MaxSupply
}
Price {
Ohlc {
Open
Low
High
Close
}
IsQuotedInUsd
Average {
Mean
WeightedSimpleMoving
SimpleMoving
ExponentialMoving
}
}
Currency {
Symbol
Name
Id
}
Block {
Timestamp
}
Interval {
Time {
Duration
Start
End
}
}
}
}
}
```
#### How does the above query work?
It takes amounts and prices from all chains that use BTC and wrapped versions (including bridged versions) and presents an aggregated view. The OHLC, mean and other values represent a stable BTC picture.
### Tokens
Let's say you don't want a chain agnostic view, but want to focus on a particular chain. How to stream or query prices for it? This is where tokens come in.
> **Note:** the price below is blended across **all pools** where the token is base. For a single token, the [rank-1 Pairs query](/docs/trading/crypto-price-api/pairs#most-accurate-token-price) prices it from its top market instead — recommended whenever you care about one specific token.
> Note: We include `Volume: {Usd: {gt: 5}}` to further remove extreme outliers; the stream already pre-filters outliers—this is an additional check.
```graphql
{
Trading {
Tokens(
where: {
Token: {
Address: { is: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }
Network: { is: "Solana" }
}
Interval: { Time: { Duration: { eq: 1 } } }
Volume: { Usd: { gt: 5 } }
}
limit: { count: 1 }
orderBy: { descending: Block_Time }
) {
Token {
Address
Id
IsNative
Name
Network
Symbol
TokenId
}
Block {
Date
Time
Timestamp
}
Interval {
Time {
Start
Duration
End
}
}
Volume {
Base
BaseAttributedToUsd
Quote
Usd
}
Supply {
MarketCap
FullyDilutedValuationUsd
CirculatingSupply
TotalSupply
MaxSupply
}
Price {
IsQuotedInUsd
Ohlc {
Close
High
Low
Open
}
Average {
ExponentialMoving
Mean
SimpleMoving
WeightedSimpleMoving
}
}
}
}
}
```
We filter for specific tokens using the token `Address` and `Network`. If you need to stream all token prices on say Solana, simply set `Network` to `solana`.
> Note: We include `Volume: {Usd: {gt: 5}}` to further remove extreme outliers; the stream already pre-filters outliers—this is an additional check.
```graphql
subscription {
Trading {
Tokens(
where: {
Token: { Network: { is: "Solana" } }
Volume: { Usd: { gt: 5 } }
}
) {
Token {
Address
Id
IsNative
Name
Network
Symbol
TokenId
}
Block {
Date
Time
Timestamp
}
Interval {
Time {
Start
Duration
End
}
}
Volume {
Base
BaseAttributedToUsd
Quote
Usd
}
Supply {
MarketCap
FullyDilutedValuationUsd
CirculatingSupply
TotalSupply
MaxSupply
}
Price {
IsQuotedInUsd
Ohlc {
Close
High
Low
Open
}
Average {
ExponentialMoving
Mean
SimpleMoving
WeightedSimpleMoving
}
}
}
}
}
```
### Pairs
This is the 3rd cube in these set of APIs. The Pairs cube gives you price, volume, and market-level trading data between two tokens — a base token and a quote token.
We will breakdown in detail how base token and Quote are chosen in the next section.
> **Recommended for single-token prices:** add `Ranking: { Position: { eq: 1 } }` to a Pairs query to get a token's price from its **top market** — the pool carrying the most volume for it. See [Getting the Most Accurate Token Price](/docs/trading/crypto-price-api/pairs#most-accurate-token-price).
> **Tip**: Use `TokenId` instead of `Token.Address` to fetch all variants of the same token (e.g., ETH, WETH, bridged ETH) across multiple chains.
> Note: We include `Volume: {Usd: {gt: 5}}` to further remove extreme outliers; the stream already pre-filters outliers—this is an additional check.
```graphql
subscription {
Trading {
Pairs(
where: {
Market: { Network: { is: "Base" } }
QuoteToken: {
Address: { is: "0x4200000000000000000000000000000000000006" }
}
Token: { Address: { is: "0x940181a94a35a4569e4529a3cdfb74e38fd98631" } }
Volume: { Usd: { gt: 5 } }
}
) {
Currency {
Symbol
Name
Id
}
Market {
Protocol
Program
Network
Name
Address
}
Token {
Address
Id
Name
Symbol
TokenId
}
Volume {
Usd
Base
Quote
BaseAttributedToUsd
}
Supply {
MarketCap
FullyDilutedValuationUsd
CirculatingSupply
TotalSupply
MaxSupply
}
QuoteToken {
TokenId
Name
Id
Address
Symbol
}
QuoteCurrency {
Id
}
Price {
Ohlc {
Open
Low
High
Close
}
Average {
WeightedSimpleMoving
SimpleMoving
Mean
ExponentialMoving
}
}
}
}
}
```
## Understanding Intervals
Unlike DEXtrades APIs, the intervals here are fixed and cannot be arbitrary.
### Supported Time Intervals
The following durations (in seconds) are supported for querying or streaming historical and real-time data, unlike DEX APIs, these intervals are fixed, other values are not supported.
`1, 3, 5, 10, 30, 60, 300, 900, 1800, 3600`
### Supported Volume Aggregation Levels
Use `TargetVolume` to get price intervals aggregated over a volume threshold:
`1000, 10000, 100000, 1000000 (USD)`
## When to Choose Which Cube (Token, Currency, Pair)?
:::tip The short answer
For the price of a **specific token**, use **`Pairs` with `Ranking: { Position: { eq: 1 } }`** — see [Getting the Most Accurate Token Price](/docs/trading/crypto-price-api/pairs#most-accurate-token-price). Use `Tokens` for chain-wide streams and per-chain aggregates, and `Currencies` for one cross-chain number per asset.
:::
### Use the **`Pairs`** cube when:
- You want the **price of a specific token** — filter to its top market with `Ranking: { Position: { eq: 1 } }` so thin pools do not affect the number ([how and why](/docs/trading/crypto-price-api/pairs#most-accurate-token-price)).
- You want **pair-level trading data** (e.g., SOL/USDC, ETH/DAI).
- You’re analyzing trading activity **on a specific market or DEX** (e.g., Uniswap, PancakeSwap, Raydium).
- You need **OHLC, volume, liquidity**, and market-specific pricing between two tokens.
- You're exploring **price arbitrage** or spreads across chains or platforms.
### Use the **`Tokens`** cube when:
- You want a **firehose** of price updates for every token on a chain.
- You want **one chain-wide number per token**, blended across all of its pools, rather than the price on a single market.
- You're interested in **aggregate volume across all of a token's pools**, or the currency-level `Supply` and `MarketCap` fields.
> For a **single token's price**, prefer `Pairs` with rank 1: the `Tokens` price is a volume-weighted blend of every pool, so thin pools can pull it away from the primary market. See [Tokens cube](/docs/trading/crypto-price-api/tokens).
### Use the **`Currencies`** cube when:
- You want a **chain-agnostic view** of a token (e.g., BTC across Bitcoin, Ethereum (WBTC), Solana, etc.).
- You need a **global price** for a currency, combining its various representations.
- You're looking for **aggregated OHLC and average prices** for a token across chains.
## Crypto Price API for TradingView
With the new Price API, you can simply feed the stream to your custom datafeed object in the TradingView code and have it update charts in real-time.
- A sample tutorial is available [here](/docs/usecases/tradingview-subscription-realtime/getting-started/)
- The stream is available ready-to-chart SDK [here](https://www.npmjs.com/package/@bitquery/tradingview-sdk). Simply copy paste the advanced charting library into the correct folder, add Bitquery access token and it is ready.
## Video Tutorial | Introduction to Crypto Price APIs
'; echo formatArrayAsTable($sandwichOpportunityData); echo ''; echo ''; ``` The formatArrayAsTable() method is used to render the array as a table. ### Adding a Table #### Adding table headers This function will be used to get the column headings for the table. ```php function getTableHeaders($data) { $headers = []; foreach ($data as $row) { foreach ($row as $key => $value) { if (!in_array($key, $headers)) { $headers[] = $key; } } } return $headers; } ``` #### Helper function This function will be used to recursively flattens the nested arrays and objects into a single-level array with concatenated keys. ```php function flattenArray($array, $prefix = '') { $result = []; foreach ($array as $key => $value) { $newKey = $prefix ? "{$prefix}_{$key}" : $key; if (is_array($value)) { $result = array_merge($result, flattenArray($value, $newKey)); } else { $result[$newKey] = $value; } } return $result; } ``` Finally this code snippet takes up the latest DEX trades with large purchase amount from the EVM chains, and displays them in a table using simple HTML. ```php function formatArrayAsTable($data) { $flattenedData = array_map('flattenArray', $data); $html = '
| ' . htmlspecialchars($column) . ' | '; } $html .= '
|---|
| ' . htmlspecialchars($row[$column] ?? '') . ' | '; } $html .= '
BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)BalanceUpdates version (stops working 10 August 2026)Status: {isConnected ? "Connected" : "Disconnected"}
{error &&Error: {error}
} {data ? (| Protocol Family | {data.Solana.DEXTrades[0].Trade.Dex.ProtocolFamily} |
|---|---|
| Protocol Name | {data.Solana.DEXTrades[0].Trade.Dex.ProtocolName} |
| Buy Amount | {data.Solana.DEXTrades[0].Trade.Buy.Amount} |
| Buy Currency | {data.Solana.DEXTrades[0].Trade.Buy.Currency.Symbol} |
| Sell Amount | {data.Solana.DEXTrades[0].Trade.Sell.Amount} |
| Sell Currency | {data.Solana.DEXTrades[0].Trade.Sell.Currency.Symbol} |
| Transaction Signature | {data.Solana.DEXTrades[0].Transaction.Signature} |
Loading data...
)}BalanceUpdates version (stops working 10 August 2026)
Aggregations (pool, token, and **non-stablecoin** currency) use these **decayed** volume contributions instead of raw sums over the window.
## How Token Prices Are Determined
Token prices are determined by **volume-weighted aggregation** over eligible trades in the **last hour**, using decayed volumes as above.
### Step 1: Identify pairs where the token is base
We determine in which pairs the token acts as the **base** currency. A token can be quote in one pair (e.g. PUMP/WSOL) and base in another (e.g. WSOL/USDT); for token price we only use pairs where the token is **base**.
### Step 2: Aggregate from pools to token price
For each such pair we use the markets (DEX pools) where it is traded. Conceptually, for pools that trade the same logical pair, the combined price uses **volume-weighted** pooling with **decayed** base-token volumes over the 1-hour window:
```
price(USD) = sum( price(p) × effectiveVolume(p) ) / sum( effectiveVolume(p) )
```
Where:
- **price(p)** = latest price of a trade in pool `p` (in USD; see [How Pool Prices Are Normalized to the Current Quote Token](#how-pool-prices-are-normalized-to-the-current-quote-token) below).
- **effectiveVolume(p)** = sum of **base-token** amounts for trades in pool `p` in the window, each multiplied by **`exp(-decayRate × age)`** for that trade.
The **token** price is then the same style of **volume-weighted** combination across all pairs where the token is base, again using decayed volumes.
## How Currency Prices Are Determined
The same rules apply for **non-stablecoin** currencies: a **1-hour** window, **exponentially decayed** volume weights (50% at the midpoint), and aggregation over **tokens** (volume and prices of each token representation) instead of over pairs and pools. Currency price is the volume-weighted combination of its token representations (e.g. WBTC, cbBTC, etc.) across chains.
**Stablecoins** are **not** included in this trade-based currency path. Processing skips them in the currency aggregation loop that builds prices from DEX trades; stablecoin USD (or peg) prices come from a **separate pipeline** fed by an **external** spot source, **not** from decay-weighted DEX trade aggregation.
**Ranking** is **not** exposed on currency-level responses. Internal logic may still derive token→currency weights while computing prices, but only **pair** and **token** updates are given a **`Ranking`** on the outbound message—do not look for **`Ranking`** on currency entities in the API.
## Ranking on Trades, Pairs, and Tokens
The API for **Trades**, **Pairs**, and **Tokens** exposes a **Ranking** object with two fields. It does **not** appear on **currencies** (see [How Currency Prices Are Determined](#how-currency-prices-are-determined)):
| Field | Meaning |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Position** | **1-based** index describing **priority** when multiple sources contribute: order of the **pair** in **token price**, or of the **token** among representations that feed a **non-stablecoin** currency blend. Lower number means higher priority in that ordering. |
| **Weight** | Float in **`[0, 1]`** — this pool's share of the total decay-weighted volume at the current moment. Reflects how much this pool drives the blended token price. Weights across all contributing pools for a given token sum to 1. |
Use **Position** for display order or precedence; use **Weight** for how much each contributor matters in the blended price.
:::tip Practical use: pricing a token from its top market
Filtering **`Pairs`** to **`Ranking: { Position: { eq: 1 } }`** gives you a token's price on the single market contributing the most decay-weighted volume, instead of the blend across all of its pools. This is the recommended way to price a specific token — see [Getting the Most Accurate Token Price](/docs/trading/crypto-price-api/pairs#most-accurate-token-price) for queries, streaming, and caveats.
:::
### Example: filter by ranking position
Filter pairs in **`where`** using **`Ranking.Position`** when you only want rows that are among the top contributors to the token price (for example positions **1**, **2**, and **3**). Request **`Ranking { Position Weight }`** on each pair to see both the rank and that pair’s **relative weight** in the blend. **`Weight`** is normally used as a returned field rather than as a filter.
```graphql
{
Trading {
Pairs(
where: {
Ranking: { Position: { in: [1, 2, 3] } }
Interval: { Time: { Duration: { eq: 3600 } } }
Block: { Time: { since_relative: { hours_ago: 1 } } }
}
limit: { count: 10 }
) {
Block {
Time(maximum: Block_Time)
}
Currency {
Id
}
Token {
Id
}
QuoteToken {
Id
}
Market {
Id
Protocol
Address
}
Price {
Ohlc {
Close
}
}
Ranking {
Position
Weight
}
Interval {
Time {
Start
End
}
}
}
}
}
```
## Volume and Amounts: Quote Token Amounts vs USD
When pricing is in USD (USD-base):
- **GraphQL** `Volume { Quote }` and **Protobuf** `Amounts.Quote` = **sum of quote token amounts** (e.g. sum of USDT, USDC, etc. amounts), **not** sum of USD.
- For **USD amounts** use **`Volume { Usd }`** (GraphQL) and **`Amounts.Usd`** (Protobuf) as before.
So for USD-based pricing, use `Volume.Usd` / `Amounts.Usd` when you need USD totals; use `Volume.Quote` / `Amounts.Quote` when you need the total in quote token units.
## How Pool Prices Are Normalized to the Current Quote Token
The **latest price of a trade in a pool** is not taken at the time that trade happened. It is **normalized** using the **current** price of the quote token at the time when the weighting is performed. So the quote side of the price is evaluated at weighting time, not at the time of the last trade in the pool. This keeps aggregated prices consistent with current quote token (e.g. stablecoin) valuation.
---
## Private Queries in Bitquery IDE
URL: https://docs.bitquery.io/docs/ide/private/
Private Queries in Bitquery IDE in Bitquery docs with practical setup steps, examples, and guidance for secure API access.
# Save Query Privately
In the IDE you can save your queries privately, so that only you will be able to see that query. To do this you will have to select the `Private` checkbox and then click the `Save` button.

Use private queries for work you don't want surfaced in public search — internal dashboards, client work, or queries containing specific addresses. Leave the checkbox unchecked to share a query publicly so others can find it via [search](/docs/ide/search/).
## Next steps
- [Create a query](/docs/ide/query/)
- [Search queries](/docs/ide/search/)
- [Share a query](/docs/ide/share/)
---
## Pump.fun API - Live Prices, OHLCV, ATH, MarketCap
URL: https://docs.bitquery.io/docs/blockchain/Solana/Pumpfun/Pump-Fun-API/
Stream Pump.fun trades, new token launches, OHLCV, bonding curve progress and top traders on Solana over GraphQL, WebSocket, gRPC or Kafka.
# Pump.fun API - Live Prices, OHLCV, ATH, MarketCap
:::tip Want structured trades, OHLC and market cap? Start with the Trading API
The [**Trading API**](/docs/trading/trading-data-overview) is the fastest path to clean Pump.fun market data. [`Trading.Trades`](/docs/trading/crypto-trades-api/trades-api) returns **MEV-filtered swaps with USD price, market cap and supply on every row**, across **9 chains in one API**, and [`Trading.Tokens`](/docs/trading/crypto-price-api/tokens) gives pre-aggregated OHLC down to one second — so you never build candles yourself. Worked Pump.fun examples are in [Trader-Focused Trade APIs](#trader-focused-trade-apis-with-usd-price-market-cap--supply) below.
Reach for the raw chain-level queries on this page when you need what the Trading API deliberately does not carry: **history older than the Trading window**, **bonding-curve internals**, per-instruction detail, or call / event context.
:::
New here? The [Pump.fun API product page](https://bitquery.io/products/pumpfun-api) covers delivery channels, latency, pricing and the free trial; this page is the query reference.
The Bitquery Pump.fun API provides real-time and historical data for Pump.fun memecoin trades on Solana via GraphQL. Use the API to get live trades, new token launches, OHLCV, bonding curve progress, top traders, and tokens migrated to PumpSwap. Access it via REST, WebSocket subscriptions, gRPC streams, or Kafka, and filter by the program address `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`. For other data points, reach out to [support](https://t.me/Bloxy_info).
:::tip **Try Our PumpFun Token Sniffer**
Want to analyze Pump.fun tokens for potential phishing risks? Check out our [**PumpFun Token Sniffer**](https://pumpfun-token-sniffer.vercel.app/) — a proof-of-concept tool that helps identify suspicious token behavior by analyzing transfers and trades.
:::
Need zero-latency Pump.fun data? [Read about our Shred Streams and contact us for a trial](/docs/streams/real-time-solana-data/). For gRPC streaming, see [Pump.fun gRPC Streams →](/docs/grpc/solana/examples/pump-fun-grpc-streams/).
You may also be interested in:
- [LetsBonk.fun APIs ➤](/docs/blockchain/Solana/letsbonk-api/)
- [PumpSwap APIs ➤](/docs/blockchain/Solana/Pumpfun/pump-swap-api/)
- [Moonshot APIs ➤](/docs/blockchain/Solana/Moonshot-API/)
- [FourMeme APIs ➤](/docs/blockchain/BSC/four-meme-api/)
- [DEXrabbit Pump.fun tokens](https://dexrabbit.bitquery.io/categories/pump-fun) — live multi-chain DEX prices and 24h volume
:::note
To query or stream data via graphQL **outside the Bitquery IDE**, you need to generate an API access token.
Follow the steps here to create one: [How to generate Bitquery API token ➤](/docs/authorization/how-to-generate/)
:::
## What is the Program Address for Pump.fun on Solana?
The Pump.fun program address on Solana mainnet is `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`. Use Bitquery's GraphQL API to filter DEX trades, token creation, and bonding curve data by this program ID.
```
6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
```
Use this program ID when filtering Solana instructions or token supply updates for Pump.fun tokens. All examples below reference this program. For bonding curve data, token creation, and migration tracking, use the queries in this guide with `Program: { Address: { is: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" } }`.
## Token Creation & Metadata
### How do I get newly created Pump.fun tokens?
Use Bitquery's `TokenSupplyUpdates` subscription filtered by the Pump.fun program `create` or `create_v2` method to get metadata, supply, and dev address of newly created tokens in real time.
[Try New Pump.fun Tokens Query ➤](https://ide.bitquery.io/newly-created-PF-token-dev-address-metadata_8)
## Overview
The Pump.fun Token Sniffer is a Python-based analysis tool built with Bitquery APIs that displays various metrics about Pump.fun tokens. The tool provides:
### Token Information Metrics
- **Bonding Curve Address**: Automatically detects and displays the bonding curve address
- **Token Creation Details**: Creator address, creation timestamp, and transaction signature
- **Token Metadata**: Name, symbol, IPFS metadata (image, website, social links, description)
- **Token Status**: Mayhem mode status and graduation status (whether token has migrated to PumpSwap)
### Holder Distribution Metrics
- **Top 10 Holders**: List of addresses with the largest token holdings
- **Holding Percentages**: Percentage of total supply held by each top holder
- **Creator Holdings**: Percentage of tokens held by the creator (if in top 10)
- **Top 10 Concentration**: Total percentage of supply held by top 10 holders
- **Holder Distribution Checks**: Flags if any single holder (excluding bonding curve) holds more than 5% of supply
### Holder Activity Metrics
- **Pump Token Portfolio**: Number of different Pump.fun tokens each holder owns
- **Trading Activity**: Number of trades executed by each holder in the last 6 hours
- **Holder Engagement**: Activity level indicators based on recent trading
### Transfer and Purchase Analysis Metrics
- **Transfer Statistics**: Total addresses that received token transfers
- **Purchase Statistics**: Number of addresses that purchased the token
- **Transfer vs Purchase Comparison**: Analysis of addresses that received transfers vs those that made purchases
- **Transfer Timing Analysis**: Comparison of first transfer timestamps vs first purchase timestamps
- **Transfer Amounts**: Total amounts transferred, purchased, and transferred without corresponding purchases
### Liquidity Metrics
- **Pool Liquidity**: Current SOL liquidity in the bonding curve pool
### Interface Options
- **Web UI**: Modern web interface for interactive analysis
- **Command Line**: Terminal-based interface for quick checks
## Prerequisites
1. **Python 3.8+** installed on your system
2. **Bitquery API Token** - Get your API token [here](/docs/authorization/how-to-generate/)
3. Basic understanding of Solana blockchain and Pump.fun tokens
## Installation
1. Clone the repository:
```bash
git clone https://github.com/Akshat-cs/pumpfun-token-sniffer
cd pumpfun-token-sniffer
```
2. Install the required dependencies:
```bash
pip install -r requirements.txt
```
3. Set up your API key:
- Copy `.env.sample` to `.env`:
```bash
cp .env.sample .env
```
- Edit `.env` and replace `your_api_key_here` with your actual Bitquery API key:
```
BITQUERY_API_KEY=BQ_your_actual_api_key_here
```
## Project Structure
The project consists of two main files:
- **`check_phishy_token.py`**: Core analysis logic with GraphQL queries and detection algorithms
- **`app.py`**: Flask web application that provides the UI and API endpoints
## How It Works
The tool uses multiple GraphQL queries from Bitquery APIs to fetch and display various token metrics:
1. **Bonding Curve Detection**: Finds the bonding curve address by querying Pump.fun creation instructions
2. **Token Creation Data**: Retrieves creator address, creation time, and transaction details
3. **Token Metadata**: Fetches token name, symbol, and IPFS metadata
4. **First Transfers Query**: Fetches the first transfers of a token to various addresses
5. **First Buys Query**: Retrieves purchase data for addresses that received transfers
6. **Top Holders Query**: Gets top 10 holders with their current holdings
7. **Holder Activity Analytics**: Fetches pump token portfolio counts and recent trade activity for each holder
8. **Holder Distribution Analysis**: Calculates concentration metrics and distribution patterns
9. **Liquidity Query**: Gets current pool liquidity information
10. **Graduation Check**: Verifies if token has migrated to PumpSwap
### Transfer vs Purchase Analysis
One of the metrics the tool displays is a comparison between token transfers and purchases:
- **Addresses with transfers but no purchases**: Shows addresses that received tokens via transfer but never purchased them
- **Transfer timing vs purchase timing**: Compares when addresses first received transfers vs when they first made purchases
- **Transfer amounts**: Displays total amounts transferred, purchased, and the difference
**Note**: This metric is displayed for informational purposes. While unusual patterns (like many addresses receiving transfers without purchases) can be noteworthy, they do not definitively indicate whether a token is legitimate or not. Many factors contribute to token distribution patterns, and this tool simply presents the data for your analysis.
### Code Walkthrough
#### 1. Bonding Curve Detection
The tool first identifies the bonding curve address by querying Pump.fun creation instructions:
```python
def get_bonding_curve_address(token_address: str, api_key: str) -> Optional[Dict]:
"""
Get the bonding curve address for a Pump.fun token by querying the create instruction.
The bonding curve is the 3rd account in the Instruction accounts array.
"""
query = """
query MyQuery($token: String) {
Solana {
Instructions(
where: {
Instruction: {
Program: {
Address: {is: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"}
Method: {in: ["create", "create_v2"]}
}
Accounts: {includes: {Address: {is: $token}}}
}
Transaction: {Result: {Success: true}}
}
) {
Block {
Creation_time: Time
}
Instruction {
Accounts {
Address
}
Program {
AccountNames
Method
Arguments {
Name
Type
Value {
... on Solana_ABI_String_Value_Arg {
string
}
... on Solana_ABI_Boolean_Value_Arg {
bool
}
}
}
}
}
Transaction {
Creation_transaction: Signature
DevAddress: Signer
}
}
}
}
"""
```
The bonding curve is extracted from the 3rd account (index 2) in the accounts array. This query also extracts token metadata (name, symbol, URI), creator address, creation time, and Mayhem mode status.
#### 2. First Transfers Query
This query fetches the first transfers of the token to addresses (excluding the bonding curve):
```python
def get_first_transfers_pumpfun(token_address: str, bonding_curve: str, api_key: str) -> List[Dict]:
"""
Query 1: Get the first transfers of a Pump.fun token to addresses (Solana).
"""
query = """
query MyQuery($token: String, $bonding_curve: String) {
Solana {
Transfers(
limit: { count: 1000 }
orderBy: { ascendingByField: "Block_first_transfer" }
where: {
Transfer: {
Receiver: {
Token: {
Owner: {
not: $bonding_curve
notIn: ["8psNvWTrdNTiVRNzAgsou9kETXNJm2SXZyaKuJraVRtf", "AkTgH1uW6J6j6QHmFNGzZuZwwXaHQsPCpHUriED28tRj"]
}
}
}
Currency: { MintAddress: { is: $token } }
}
Transaction: { Result: { Success: true } }
}
) {
Transfer {
Receiver {
Token {
Owner
}
}
}
Block {
first_transfer: Time(minimum: Block_Time)
}
total_transferred_amount: sum(of: Transfer_Amount)
}
}
}
"""
```
This query:
- Limits results to 1,000 addresses
- Excludes the bonding curve and known system addresses
- Groups by receiver address
- Gets the first transfer timestamp and total transferred amount
#### 3. First Buys Query
This query checks if the addresses that received transfers ever bought the token:
```python
def get_first_buys_pumpfun(token_address: str, buyers_list: List[str], api_key: str) -> Dict[str, Dict]:
"""
Query 2: Get first buys of an address list for a specific Pump.fun token (Solana).
"""
query = """
query MyQuery($token: String!, $buyersList: [String!]) {
Solana {
DEXTradeByTokens(
orderBy: { ascendingByField: "Block_first_buy" }
where: {
Trade: {
Account: { Token: { Owner: { in: $buyersList } } }
Currency: { MintAddress: { is: $token } }
Side: { Type: { is: buy } }
}
Transaction: { Result: { Success: true } }
}
) {
Trade {
Account {
Token {
Owner
}
}
Currency {
Name
Symbol
MintAddress
}
Side {
Type
}
}
Block {
first_buy: Time(minimum: Block_Time)
}
total_bought_amount: sum(of: Trade_Amount)
}
}
}
"""
```
This query:
- Filters for buy-side trades only
- Groups by buyer address
- Gets the first buy timestamp and total bought amount
#### 4. Transfer vs Purchase Analysis
The analysis function compares transfers and purchases to identify patterns:
```python
def analyze_phishy_behavior_pumpfun(transfers: List[Dict], buy_data: Dict[str, Dict]) -> Tuple[int, List[Dict]]:
"""
Analyze transfers vs purchases to identify patterns for Pump.fun tokens.
"""
addresses_with_patterns = []
for transfer in transfers:
receiver = transfer["Transfer"]["Receiver"]["Token"]["Owner"]
first_transfer_time = transfer["Block"].get("first_transfer")
total_transferred = transfer.get("total_transferred_amount", 0)
buy_info = buy_data.get(receiver)
total_bought = buy_info.get("total_amount", 0) if buy_info else 0
transferred_without_buy = float(total_transferred) - float(total_bought)
if buy_info is None:
# Address received transfer but never purchased
addresses_with_patterns.append({
"address": receiver,
"first_transfer_time": first_transfer_time,
"first_buy_time": None,
"total_transferred": total_transferred,
"total_bought": 0,
"transferred_without_buy": transferred_without_buy,
"pattern": "Never bought the token"
})
else:
first_buy_time = buy_info.get("first_buy_time")
# Compare timestamps - if transfer happened before buy, note the pattern
if first_transfer_time < first_buy_time:
addresses_with_patterns.append({
"address": receiver,
"first_transfer_time": first_transfer_time,
"first_buy_time": first_buy_time,
"total_transferred": total_transferred,
"total_bought": total_bought,
"transferred_without_buy": transferred_without_buy,
"pattern": "Transfer before buy"
})
return len(addresses_with_patterns), addresses_with_patterns
```
#### 5. Top Holders Analysis
The tool also fetches top 10 holders with additional analytics:
```python
def get_top_holders_pumpfun(token_address: str, api_key: str) -> List[Dict]:
"""
Get top 10 holders of a Pump.fun token.
"""
query = """
query MyQuery($token: String) {
Solana {
BalanceUpdates(
limit: {count: 10}
orderBy: {descendingByField: "BalanceUpdate_Holding_maximum"}
where: {
BalanceUpdate: {
Currency: {MintAddress: {is: $token}}
}
Transaction: {Result: {Success: true}}
}
) {
BalanceUpdate {
Currency {
Name
MintAddress
Symbol
}
Account {
Token {
Owner
}
}
Holding: PostBalance(maximum: Block_Slot, selectWhere: {ne: "0"})
}
}
}
}
"""
```
For each holder, the tool also fetches:
- **Pump token count**: Number of Pump.fun tokens held by the address
- **Trade activity**: Number of trades in the last 6 hours
#### 6. Web Application (Flask)
The Flask app (`app.py`) provides a REST API endpoint:
```python
@app.route(f'{APPLICATION_ROOT}/api/check', methods=['POST'])
def check_token():
"""API endpoint to analyze a token and return various metrics."""
data = request.get_json()
token_address = data.get('token_address', '').strip()
# Find bonding curve
bonding_curve_data = get_bonding_curve_address(token_address, API_KEY)
# Get transfers and analyze patterns
transfers = get_first_transfers_pumpfun(token_address, bonding_curve, API_KEY)
addresses = [t["Transfer"]["Receiver"]["Token"]["Owner"] for t in transfers]
buy_data = get_first_buys_pumpfun(token_address, addresses, API_KEY)
pattern_count, addresses_with_patterns = analyze_phishy_behavior_pumpfun(transfers, buy_data)
# Get top holders with stats
top_holders = get_top_holders_pumpfun(token_address, API_KEY)
# ... enrich with pump token counts and trade activity
return jsonify(result)
```
### Additional Features
- **Bonding Curve Detection**: Automatically finds the bonding curve address associated with the token
- **Top Holders Analysis**: Shows the top 10 holders with:
- Number of pump tokens each holds
- Recent trading activity (last 6 hours)
- Percentage of total supply
- **Holder Distribution Analysis**: Displays concentration metrics showing if creator or other holders hold more than 5% of supply
- **Token Metadata**: Fetches token name, symbol, and IPFS metadata (image, website, social links)
- **Liquidity Information**: Gets current pool liquidity
- **Graduation Check**: Verifies if token has graduated to PumpSwap (not supported)
- **Summary Statistics**: Provides aggregated totals of transferred amounts, bought amounts, and amounts transferred without purchase
## Running the Tool
### Option 1: Web UI (Recommended)
1. Start the web server:
```bash
python app.py
```
2. Open your browser and navigate to `http://localhost:8080`
3. Enter a Pump.fun token address in the input field
4. Click "Check Token" to run the analysis
The web UI features:
- Modern web3-styled interface with dark theme
- Automatic bonding curve detection
- Top 10 holders table with pump token counts and trade stats
- Clickable addresses linking to DEXrabbit
- Copy-to-clipboard functionality for addresses
- Detailed breakdown of transfer vs purchase analysis
- All token metrics displayed in organized sections
- Summary statistics for all metrics
- Responsive design
### Option 2: Command Line
Run the analysis directly from the command line:
```bash
python check_phishy_token.py WZrxegwJK4vWFGC149Ajt86vbKA9tsrJxu8mJFdpump
```
Note: The bonding curve is automatically detected, so you only need to provide the token address.
## Understanding the Output
The tool displays various metrics organized into different sections:
### Token Information
- **Bonding Curve Address**: The bonding curve program address for this token
- **Creator Address**: The wallet address that created the token
- **Creation Time**: When the token was created
- **Transaction Signature**: The transaction that created the token
- **Token Name & Symbol**: Basic token information
- **Token Metadata**: IPFS metadata including image, website, social links, and description
- **Mayhem Mode**: Whether the token was created in Mayhem mode (affects total supply)
- **Graduation Status**: Whether the token has migrated to PumpSwap
### Top Holders Metrics
For each of the top 10 holders, the tool displays:
- **Address**: The wallet address
- **Holding Amount**: Number of tokens held
- **Percentage of Supply**: What percentage of total supply this holder owns
- **Pump Token Count**: How many different Pump.fun tokens this address holds
- **Trades (Last 6h)**: Number of trades executed in the last 6 hours
### Holder Distribution Analysis
- **Creator Percentage**: If creator is in top 10, shows their holding percentage
- **Creator Check**: Indicates if creator holds less than 5% (or not in top 10)
- **Other Holders Check**: Indicates if any other holder (excluding creator) holds more than 5%
- **Top 10 Total Percentage**: Combined percentage of supply held by top 10 holders
- **Top 10 Check**: Indicates if top 10 hold less than 70% of supply
### Transfer vs Purchase Analysis Metrics
This section displays data comparing token transfers and purchases:
- **Total Addresses with Transfers**: Count of all addresses that received the token via transfer
- **Addresses with Transfers but No Purchases**: Number of addresses that received transfers but never purchased
- **Addresses with Both Transfers and Purchases**: Number of addresses that both received transfers and made purchases
For addresses that received transfers but didn't purchase (or received transfers before purchases), the tool shows:
- **Address**: The wallet address
- **First Transfer Time**: When the address first received the token
- **First Buy Time**: When the address first purchased (if any)
- **Total Transferred**: Total amount of tokens transferred to this address
- **Total Bought**: Total amount of tokens purchased by this address
- **Amount Transferred Without Buy**: Difference between transferred and bought amounts
- **Pattern Type**: Description of the pattern observed (e.g., "Never bought the token" or "Transfer before buy")
**Summary Totals**:
- Total Amount Transferred to All Addresses
- Total Amount Bought by All Addresses
- Total Amount Transferred Without Purchase
### Liquidity Metrics
- **Current Pool Liquidity**: Amount of SOL currently in the bonding curve pool
### Example Output
The command-line output displays metrics in a structured format:
```
============================================================
Checking Pump.fun token: WZrxegwJK4vWFGC149Ajt86vbKA9tsrJxu8mJFdpump
============================================================
Finding bonding curve address...
Found bonding curve: ABC123...
Found 150 addresses that received transfers
Found buy records for 45 addresses
============================================================
RESULTS
============================================================
Total addresses that received transfers: 150
Addresses with transfers but no purchases: 105
Addresses with both transfers and purchases: 45
Transfer vs Purchase Analysis:
Found 105 address(es) with transfers but no purchases:
1. Address: ABC123...
First Transfer: 2024-01-15 10:30:00 UTC
First Buy: N/A
Total Transferred: 1,000,000.00
Total Bought: 0
Amount Transferred Without Buy: 1,000,000.00
Pattern: Never bought the token
------------------------------------------------------------
TRANSFER VS PURCHASE SUMMARY:
------------------------------------------------------------
Total Amount Transferred: 5,000,000.00
Total Amount Bought: 500,000.00
Total Amount Transferred Without Purchase: 4,500,000.00
------------------------------------------------------------
```
The web UI displays all metrics in an organized dashboard format with sections for token information, top holders, holder distribution analysis, transfer vs purchase metrics, and liquidity data.
## API Endpoints
The Flask web application provides the following endpoints:
### POST `/api/check`
Analyzes a Pump.fun token for phishy behavior.
**Request Body:**
```json
{
"token_address": "WZrxegwJK4vWFGC149Ajt86vbKA9tsrJxu8mJFdpump"
}
```
**Response:**
```json
{
"success": true,
"token_address": "WZrxegwJK4vWFGC149Ajt86vbKA9tsrJxu8mJFdpump",
"token_type": "pumpfun",
"data": {
"total_addresses": 150,
"transfer_purchase_analysis": {
"addresses_with_transfers_no_purchases": 105,
"addresses_with_both": 45
},
"transfer_purchase_details": [
{
"address": "ABC123...",
"first_transfer_time": "2024-01-15T10:30:00Z",
"first_buy_time": null,
"total_transferred": "1000000.0",
"total_bought": "0",
"transferred_without_buy": 1000000.0,
"pattern": "Never bought the token"
}
],
"top_holders": [
{
"address": "XYZ789...",
"holding": "50000000",
"percent_holding": 5.0,
"pump_tokens_count": 12,
"trades_6h": 5
}
],
"bonding_curve": "BondingCurveAddress...",
"token_creation": {
"transaction_signature": "Signature...",
"creator_address": "CreatorAddress...",
"creation_time": "2024-01-15T09:00:00Z"
},
"token_metadata": {
"name": "Token Name",
"symbol": "SYMBOL",
"is_mayhem_mode": false,
"image": "https://ipfs.io/ipfs/...",
"website": "https://example.com",
"twitter": "@example"
},
"holder_analysis": {
"creator_percent": 2.5,
"creator_check_passed": true,
"other_holders_check_passed": true,
"top10_percent": 45.0,
"top10_check_passed": true
},
"liquidity_sol": 125.5,
"totals": {
"total_transferred": 5000000.0,
"total_bought": 500000.0,
"total_without_buy": 4500000.0
}
}
}
```
### GET `/api/recent-phishy`
Returns a list of recently analyzed tokens from the cache (only tokens with transfer vs purchase patterns are cached).
**Response:**
```json
{
"success": true,
"tokens": [
{
"token_address": "WZrxegwJK4vWFGC149Ajt86vbKA9tsrJxu8mJFdpump",
"token_type": "pumpfun",
"transfer_purchase_count": 105,
"timestamp": "2024-01-15T12:00:00Z",
"totals": {
"total_transferred": 5000000.0,
"total_bought": 500000.0,
"total_without_buy": 4500000.0
}
}
],
"count": 1
}
```
## Limitations
- **Supported Tokens**: Only Pump.fun tokens on Solana are supported
- **Token Age**: The tool works best with tokens created recently (bonding curve detection may fail for very old tokens)
- **Graduated Tokens**: Tokens that have graduated to PumpSwap are not supported (tool will return an error)
- **Address Limit**: Analyzes up to 1,000 addresses per token (limit in the GraphQL query)
- **Query Time**: Queries may take 10-60+ seconds depending on data size (normal for blockchain queries)
- **API Rate Limits**: Subject to Bitquery API rate limits and quota restrictions
## Use Cases
This proof-of-concept tool demonstrates how to use Bitquery APIs to build token analysis tools. It can be used for:
- **Token Research**: View various on-chain metrics about Pump.fun tokens in one place
- **Holder Analysis**: Understand token distribution and concentration patterns
- **Activity Monitoring**: See trading activity and engagement levels of top holders
- **Data Aggregation**: Collect multiple data points from different Bitquery APIs
- **Learning Tool**: Understand how to query and analyze Solana blockchain data using Bitquery
- **Foundation for Advanced Tools**: Use this as a starting point to build more sophisticated analysis systems
## Building Advanced Tools
This is a basic proof-of-concept tool that demonstrates the capabilities of Bitquery APIs. You can extend this foundation to build more advanced analysis systems by:
- Adding more sophisticated analysis algorithms
- Implementing machine learning models for pattern detection
- Creating real-time monitoring and alerting systems
- Building comprehensive risk scoring systems
- Integrating with other data sources
- Adding historical trend analysis
- Creating comparative analysis across multiple tokens
- Building portfolio tracking and management tools
The Bitquery APIs provide access to comprehensive blockchain data that can power much more advanced analysis tools than this basic example.
## Important Considerations
- **This tool displays metrics only**: It does not make definitive claims about token legitimacy or risk
- **Metrics are informational**: All data points shown are for your analysis and research purposes
- **Not investment advice**: This tool is not financial or investment advice
- **Conduct your own research**: Always perform thorough due diligence before making any decisions
- **Proof of concept**: This is a basic demonstration - you can build more advanced tools using the same Bitquery APIs
- **Token age limitations**: The tool works best with recently created tokens (bonding curve detection may fail for very old tokens)
## Support
For questions or issues:
- Check the [GitHub repository](https://github.com/Akshat-cs/pumpfun-token-sniffer) for updates and issues
- Contact Bitquery support via [Telegram](https://t.me/Bloxy_info) or create a ticket [here](https://support.bitquery.io/)
---
## Pump.fun gRPC Streams - Real-time DEX Trades
URL: https://docs.bitquery.io/docs/grpc/solana/examples/pump-fun-grpc-streams/
Pump.fun gRPC Streams - Real-time DEX Trades for Bitquery Solana gRPC (CoreCast), covering setup, filters, reliability, and stream examples.
# Pump.fun gRPC Streams
Real-time streaming of Pump.fun DEX trades, orders, and transactions via CoreCast gRPC API.
gRPC is the lowest-latency channel of the [Bitquery Pump.fun API](https://bitquery.io/products/pumpfun-api).
## Repository
🔗 [**GitHub Repository**](https://github.com/bitquery/grpc-usecase-examples/tree/main/Solana/pumpfun-example)
Clone and get started:
```bash
git clone https://github.com/bitquery/grpc-usecase-examples.git
```
## Introduction
This Node.js client allows you to stream real-time trading data from Pump.fun (Solana's popular memecoin launchpad) using the CoreCast gRPC API. Monitor track buying/selling pressure, detect whale activity, and analyze trading patterns in real-time.
**Key Features:**
- 🚀 Real-time trade streaming from Pump.fun
- 🎯 Flexible filtering by tokens, traders, and trade direction
- 💰 Separate buy and sell trade monitoring
- 📊 Performance metrics and statistics
- 🔍 Detailed trade information including accounts and currencies
- ⚡ High-performance with caching and buffering
## Quick Start
```bash
# 1. Install dependencies
npm install
# 2. Configure your filters in config.yaml
# Edit the file to set your desired token and trade filter
# 3. Run the client
node index.js
```
## Stream Types
The client supports multiple stream types:
| Stream Type | Description |
| -------------- | --------------------------------------- |
| `dex_trades` | Real-time trade events (default) |
| `dex_orders` | Order placement and cancellation events |
| `dex_pools` | Pool liquidity change events |
| `transactions` | General transaction stream |
| `transfers` | Token transfer events |
| `balances` | Balance update events |
For Pump.fun monitoring, use **`dex_trades`** (default).
## Trade Data Structure
When you stream Pump.fun trades, each message contains:
### Trade Event Structure
```javascript
{
Block: {
Slot: 370485092 // Solana block slot number
},
Transaction: {
Index: 1,
Signature: "5277PwHQ4PkKRExT45HV8X8XXDmQjWZzHK8dx5ru1eaA...",
Status: {
Success: true,
ErrorMessage: null
},
Header: {
Fee: 5000,
FeePayer: "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr",
Signer: "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr",
Accounts: [...]
},
FeeInUsd: 0.00075
},
Trade: {
InstructionIndex: 2,
Dex: {
ProgramAddress: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
ProtocolName: "pump_fun",
ProtocolFamily: "pump_fun"
},
Market: {
MarketAddress: "YcQB1hGSR9hNbJ52zrCJyMvbRViQKiaLfenrgZR9BXY",
BaseCurrency: {
Symbol: "PUMPTOKEN",
Name: "Pump Token",
MintAddress: "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump",
Decimals: 6
},
QuoteCurrency: {
Symbol: "SOL",
Name: "Wrapped SOL",
MintAddress: "So11111111111111111111111111111111111111112",
Decimals: 9
}
},
Buy: {
Amount: 100000000, // Amount of token being bought
Currency: {
Symbol: "PUMPTOKEN",
Name: "Pump Token",
MintAddress: "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump",
Decimals: 6,
Parsed: true
},
Account: {
Address: "8HqR8D9gHtN1eMJyaX7BN5PmzF5z9KgQzY4nXvFfRD8m",
IsSigner: true,
IsWritable: true
},
Order: {
OrderId: null
}
},
Sell: {
Amount: 500000000, // Amount of SOL being sold
Currency: {
Symbol: "SOL",
Name: "Wrapped SOL",
MintAddress: "So11111111111111111111111111111111111111112",
Decimals: 9
},
Account: {
Address: "8HqR8D9gHtN1eMJyaX7BN5PmzF5z9KgQzY4nXvFfRD8m",
IsSigner: true,
IsWritable: true
},
Order: {
OrderId: null
}
},
Fee: 0,
Royalty: 0,
Instruction: {
Index: 2,
Depth: 0,
Program: {
Address: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
Name: "pump_fun",
Method: "swap",
Parsed: true
},
Accounts: [...],
Logs: [...]
}
}
}
```
### Key Fields Explained
| Field | Description |
| -------------------------------- | ----------------------------------------------------------------- |
| `Trade.Buy.Amount` | Amount of token being bought (in base units) |
| `Trade.Sell.Amount` | Amount of SOL being sold (in lamports) |
| `Trade.Buy.Currency.MintAddress` | Token mint address |
| `Trade.Buy.Account.Address` | Buyer's wallet address |
| `Trade.Dex.ProgramAddress` | Always `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` for Pump.fun |
| `Trade.Market.MarketAddress` | Unique market/pool address for the token |
| `Block.Slot` | Solana block slot for timing analysis |
## Configuration Options
Edit `config.yaml` to configure your stream:
### Trade Filter Options
```yaml
trade_filter: "alltrades" # or "buys" or "sells"
```
| Value | Description |
| ----------- | ------------------------------------------------ |
| `alltrades` | Show all trades (both buys and sells) |
| `buys` | Show only trades where the token is being bought |
| `sells` | Show only trades where the token is being sold |
### Available Filters
Server-side filters (applied by CoreCast API):
| Filter | Description | Example |
| ---------- | -------------------------------------- | ------------------------------------------------------- |
| `programs` | Filter by DEX program address | Pump.fun: `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P` |
| `tokens` | Filter by token mint address(es) | Your token mint address |
| `pool` | Filter by specific market/pool address | Specific Pump.fun market |
| `traders` | Filter by wallet address(es) | Specific trader wallets |
Client-side filter (applied by this client):
| Filter | Description |
| -------------- | ------------------------------------------------ |
| `trade_filter` | Filter by trade direction (buys/sells/alltrades) |
## Filter Examples
### 1. Monitor ALL Trades for a Specific Token
Track all trading activity (both buys and sells) for a specific token on Pump.fun.
```yaml
trade_filter: "alltrades"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
tokens:
- "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump"
```
**Use Case**: General market monitoring, volume analysis
---
### 2. Monitor Only BUYS for a Specific Token
Track buying pressure - see when traders are accumulating the token.
```yaml
trade_filter: "buys"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
tokens:
- "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump"
```
**Use Case**: Track accumulation patterns, detect buying momentum
---
### 3. Monitor Only SELLS for a Specific Token
Track selling pressure - detect when traders are dumping the token.
```yaml
trade_filter: "sells"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
tokens:
- "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump"
```
**Use Case**: Detect sell pressure, identify dumps, risk monitoring
---
### 4. Monitor Multiple Tokens
Track trading activity across multiple Pump.fun tokens simultaneously.
```yaml
trade_filter: "alltrades"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
tokens:
- "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump"
- "H15fwzsYWQiGTQBn23sC2QuByu9zSvaXhaDwwVmkX5m9"
- "AnotherTokenMintAddressHere"
```
**Use Case**: Portfolio tracking, multi-token analysis
---
### 5. Monitor ALL Pump.fun Activity
Stream all trades on Pump.fun (no token filter) - useful for market-wide analysis.
```yaml
trade_filter: "alltrades"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
# No tokens filter = all tokens
```
**Use Case**: Market-wide analytics, new token discovery, volume tracking
**⚠️ Warning**: This will stream a high volume of messages. Ensure your system can handle the load.
---
### 6. Monitor Specific Trader Activity
Track all trades made by a specific wallet on Pump.fun.
```yaml
trade_filter: "alltrades"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
traders:
- "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr"
```
**Use Case**: Whale watching, copytrading, smart money tracking
---
### 7. Monitor Specific Trader's Token Buys
Track when a specific wallet buys a specific token.
```yaml
trade_filter: "buys"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
tokens:
- "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump"
traders:
- "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr"
```
**Use Case**: Copy-trading specific whales, alpha signal detection
---
### 8. Monitor Multiple Traders
Track activity from multiple wallets (whale watching).
```yaml
trade_filter: "alltrades"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
traders:
- "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr"
- "8HqR8D9gHtN1eMJyaX7BN5PmzF5z9KgQzY4nXvFfRD8m"
- "9iJkLmNoPQrsTUVxYZaBcDeFgH1JkLmNoPQrsTUVxYZ"
```
**Use Case**: Monitor multiple known profitable traders
---
### 9. Monitor Specific Market/Pool
Track all activity in a specific Pump.fun market.
```yaml
trade_filter: "alltrades"
filters:
pool:
- "YcQB1hGSR9hNbJ52zrCJyMvbRViQKiaLfenrgZR9BXY"
```
**Use Case**: Deep dive into a specific token's liquidity pool
---
### 10. Detect Large Buys (Whale Accumulation)
Monitor buy trades and filter large amounts in your application logic.
```yaml
trade_filter: "buys"
filters:
programs:
- "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
tokens:
- "7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump"
```
Then in your code, filter by `Trade.Buy.Amount` or `Trade.Sell.Amount` (SOL value).
**Use Case**: Whale buy alerts, large transaction monitoring
## Output Example
When a trade matches your filters, you'll see:
```
================================================================================
🟢 BUY Trade
================================================================================
Block Slot: 370485092
Timestamp: 2025-10-01T13:11:32.922Z
Instruction Index: 2
📍 DEX Info:
Program: 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
Protocol: pump_fun (pump_fun)
🏪 Market Info:
Address: YcQB1hGSR9hNbJ52zrCJyMvbRViQKiaLfenrgZR9BXY
Base Currency: PUMPTOKEN
Quote Currency: SOL
💰 Buy Side:
Amount: 100000000
Currency: PUMPTOKEN (Pump Token)
Mint: 7CyJ5J3tqRKKJjSWSzASuVJdD2oryAJZfyvbMVJmpump
Decimals: 6
Account: 8HqR8D9gHtN1eMJyaX7BN5PmzF5z9KgQzY4nXvFfRD8m
Is Signer: true
Is Writable: true
Order ID: undefined
💸 Sell Side:
Amount: 500000000
Currency: SOL (Wrapped SOL)
Mint: So11111111111111111111111111111111111111112
Decimals: 9
Account: 8HqR8D9gHtN1eMJyaX7BN5PmzF5z9KgQzY4nXvFfRD8m
Is Signer: true
Is Writable: true
Order ID: undefined
💵 Fee: 0
👑 Royalty: 0
================================================================================
```
---
## Pump.fun to PumpSwap API - Token Migration Tracking
URL: https://docs.bitquery.io/docs/blockchain/Solana/Pumpfun/pump-fun-to-pump-swap/
Track Pump.fun tokens graduating to PumpSwap on Solana: detect migration events, the final bonding-curve trade, and post-migration AMM activity.
# Understanding Pump.fun: From Launchpad to PumpSwap
:::tip Need real-time Pump.fun & PumpSwap data or anything from the last ~30 days?
For **real-time + last ~30 days**, use the [**Trading cube**](/docs/trading/trading-data-overview) — [`Trading.Trades`](/docs/trading/crypto-trades-api/trades-api) gives you clean, MEV-filtered Pump.fun & PumpSwap swaps with **USD price, market cap, and supply on every row** across **9 chains in one API**. Use this page when you need **historical Pump.fun & PumpSwap data older than ~30 days**, raw per-swap detail, or call / event context.
Post-migration AMM activity is covered end-to-end by the [PumpSwap API](https://bitquery.io/products/pumpswap-api) — the product page lists swaps, pools and OHLCV coverage with plans.
:::
For the productized feed of these migration events — channels, pricing, free trial — see the [Pump.fun API product page](https://bitquery.io/products/pumpfun-api).
Pump.fun is a Solana-based memecoin launchpad that has reshaped how tokens are created and traded. At its core is a bonding curve model that lets anyone launch a token with a fixed supply of 1 billion tokens, of which around 800 million are made available for bonding.
As traders buy into the curve, the price increases non-linearly—early buyers benefit the most. The bonding curve progress, a very useful metric for developers and traders, can be computed as:
:::note
**Simplified Formula**:
BondingCurveProgress = 100 - (((balance - 206900000) \* 100) / 793100000)
:::
where `balance` is the balance of the bonding curve for that specific token.
This helps identify tokens nearing sell-out, a common signal for tokens “about to pump.”
Once a token reaches full bonding (100%), it automatically migrates to PumpSwap, Pump.fun’s native AMM DEX. From there, it trades like any other Solana token—no manual listing is needed; the system handles everything.
To help developers, traders, and analysts follow this journey, Bitquery offers a comprehensive real-time API suite that spans the entire lifecycle of a Pump.fun token. Let’s walk through it:
> **⚠️ Important: Proof of Concept**
>
> This tool is provided as a **proof of concept** for educational purposes. The detection logic may generate false positives and should be modified based on your requirements. Always verify alerts by checking on-chain transaction data before taking action.
GitHub Repository: [realtime-liquidity-drain-detector](https://github.com/Akshat-cs/realtime-liquidity-drain-detector)
## Overview
The Realtime Liquidity Drain Detector is a Python-based security tool that:
- Monitors DEX pools in real-time using Bitquery's Kafka streams
- Detects significant liquidity drops that may indicate malicious activity
- Provides a web dashboard for instant alerts and monitoring
- Configurable thresholds for warning and critical alerts
- Supports multiple DEX protocols including Uniswap V2, V3, and V4
## Prerequisites
1. **Python 3.8+** installed on your system
2. **Bitquery Kafka Credentials** - Contact sales via [Telegram](https://t.me/Bloxy_info) or fill out the [form](https://bitquery.io/forms/api) for Kafka access
3. **Bitquery API Token** - Get your API token [here](/docs/authorization/how-to-generate/)
4. Basic understanding of Kafka streams and Python
> **Note:** IDE credentials will not work with Kafka Streams. You need separate Kafka credentials for this tool.
## Installation
1. Clone the repository:
```bash
git clone https://github.com/Akshat-cs/realtime-liquidity-drain-detector
cd realtime-liquidity-drain-detector
```
2. Install the required dependencies:
```bash
pip install -r requirements.txt
```
3. Configure your credentials:
- Copy `config.sample.py` to `config.py`:
```bash
cp config.sample.py config.py
```
- Edit `config.py` with your Kafka credentials:
```python
username = "your_kafka_username"
password = "your_kafka_password"
```
## Configuration
> **Note:** The default configuration values are starting points for a proof of concept. You should **tune these parameters** based on your specific requirements and testing to minimize false positives and optimize detection accuracy for your use case.
Edit thresholds in `detection_config.py` to customize alert sensitivity:
```python
class DetectionConfig:
# Liquidity drop percentage thresholds
LIQUIDITY_DROP_WARNING = 20.0 # 20% drop = warning level alert
LIQUIDITY_DROP_CRITICAL = 40.0 # 40% drop = critical level alert
# Time windows
BASELINE_WINDOW_HOURS = 24 # Hours of data to build baseline
LOOKBACK_WINDOW_MINUTES = 30 # Minutes to check for recovery (false positive filter)
RAPID_DRAIN_WINDOW_MINUTES = 5 # Window for detecting sudden drains
# Max trade size decrease thresholds
MAX_AMOUNT_DECREASE_WARNING = 30.0 # 30% decrease = warning
MAX_AMOUNT_DECREASE_CRITICAL = 50.0 # 50% decrease = critical
# Alert management
ALERT_COOLDOWN_MINUTES = 10 # Minutes between alerts for same pool
RECOVERY_CHECK_ENABLED = True # Check if liquidity recovers quickly (false positive filter)
# False positive prevention
DROP_CONFIRMATION_COUNT = 2 # Number of consecutive measurements showing drop required before alerting
ENABLE_MAX_AMOUNT_DECREASE_ALERTS = False # Set to False to disable max_amount_decrease alerts
# Pool size filter
MIN_LIQUIDITY_TOKENS = 1000 # Minimum liquidity to monitor (filters out very small pools)
# Baseline reliability
MIN_MEASUREMENTS_FOR_BASELINE = 10 # Need at least 10 measurements before alerting
MIN_TIME_FOR_BASELINE_MINUTES = 5 # Need at least 5 minutes of data before alerting
```
## Running the Tool
### Start the API Server (Frontend Dashboard)
The API server provides a web dashboard for viewing alerts:
```bash
python api_server.py
```
Access the dashboard at: `http://localhost:5001`
### Start the Detector
The detector monitors Kafka streams and sends alerts to the API server:
```bash
python liquidity_drain_detector.py
```
The detector will automatically send alerts to the API server when liquidity drains are detected.
## How It Works
### 1. Real-Time Monitoring
The tool connects to Bitquery's Kafka streams for Ethereum DEX pools using the topic:
- **`eth.dexpools.proto`**
The Kafka consumer is configured with:
```python
conf = {
'bootstrap.servers': 'rpk0.bitquery.io:9092,rpk1.bitquery.io:9092,rpk2.bitquery.io:9092',
'group.id': f'{username}-liquidity-drain-{group_id_suffix}',
'session.timeout.ms': 30000,
'security.protocol': 'SASL_PLAINTEXT',
'ssl.endpoint.identification.algorithm': 'none',
'sasl.mechanisms': 'SCRAM-SHA-512',
'sasl.username': config.username,
'sasl.password': config.password,
'auto.offset.reset': 'latest',
}
```
Kafka streams provide:
- Lower latency due to shorter data pipeline
- Better reliability with persistent connections
- Ability to read from latest offset without gaps
- Better scalability with multiple consumers
For detailed information on Kafka streams, refer to the [Kafka Streaming Concepts documentation](/docs/streams/kafka-streaming-concepts/).
### 2. Data Structures
The tool uses three main data structures:
#### PoolState
Tracks the current state of a liquidity pool:
```python
@dataclass
class PoolState:
pool_id: str
pool_address: str
currency_a: str
currency_b: str
currency_a_symbol: str
currency_b_symbol: str
dex_protocol: str
amount_a: float = 0.0 # Human-readable liquidity for currency A
amount_b: float = 0.0 # Human-readable liquidity for currency B
slippage_at_bps_a_to_b: Dict[int, Dict] # Slippage data for A->B swaps
slippage_at_bps_b_to_a: Dict[int, Dict] # Slippage data for B->A swaps
price_a_to_b: float = 0.0
price_b_to_a: float = 0.0
last_updated: datetime
```
#### PoolHistory
Maintains historical data for baseline calculation and drain detection:
```python
@dataclass
class PoolHistory:
pool_id: str
amount_a_history: deque # Historical liquidity for currency A
amount_b_history: deque # Historical liquidity for currency B
max_amount_history_a_to_b_100bp: deque # Max trade size history (A->B at 100bp)
max_amount_history_b_to_a_100bp: deque # Max trade size history (B->A at 100bp)
baseline_amount_a: Optional[float] # Baseline liquidity for currency A
baseline_amount_b: Optional[float] # Baseline liquidity for currency B
baseline_max_amount_a_to_b_100bp: Optional[float]
baseline_max_amount_b_to_a_100bp: Optional[float]
recent_drop_measurements_a: deque # Track recent drops for confirmation
recent_drop_measurements_b: deque
```
### 3. Liquidity Drain Detection
The detector processes pool events from Kafka and performs three types of checks:
#### A. Liquidity Drop Detection
Tracks each currency separately and detects drops from baseline:
```python
def _check_liquidity_drop(self, state: PoolState, history: PoolHistory,
current_amount_a: float, current_amount_b: float,
current_time: datetime) -> List[DrainAlert]:
# Calculate drop percentage for each currency
drop_a, drop_b = history.amount_drop_percent(current_amount_a, current_amount_b)
# Pre-alert confirmation: Require multiple consecutive measurements
if len(history.recent_drop_measurements_a) >= DROP_CONFIRMATION_COUNT:
if drop_a >= LIQUIDITY_DROP_CRITICAL:
severity = 'critical'
elif drop_a >= LIQUIDITY_DROP_WARNING:
severity = 'warning'
# Generate alert...
```
**Key Features:**
- Tracks each currency (A and B) separately
- Requires `DROP_CONFIRMATION_COUNT` consecutive measurements showing drop
- Uses mean of last 24 hours as baseline
- Checks for recovery to filter false positives
#### B. Max Amount Decrease Detection
Monitors when maximum trade sizes decrease significantly:
```python
def _check_max_amount_decrease(self, state: PoolState, history: PoolHistory,
max_amount_a_to_b: Optional[float],
max_amount_b_to_a: Optional[float],
current_time: datetime) -> List[DrainAlert]:
# Check both swap directions
decrease_a = history.max_amount_decrease_percent(max_amount_a_to_b, 'a_to_b')
decrease_b = history.max_amount_decrease_percent(max_amount_b_to_a, 'b_to_a')
# Use worst-case direction
if worst_decrease >= 50.0:
severity = 'critical'
elif worst_decrease >= 30.0:
severity = 'warning'
```
**Note:** This alert type is disabled by default (`ENABLE_MAX_AMOUNT_DECREASE_ALERTS = False`) as it can cause false positives.
#### C. Rapid Drain Detection
Detects sudden drops within a short time window:
```python
def _check_rapid_drain(self, state: PoolState, history: PoolHistory,
current_amount_a: float, current_amount_b: float,
current_time: datetime) -> List[DrainAlert]:
# Check if liquidity dropped rapidly within RAPID_DRAIN_WINDOW_MINUTES
if history.is_rapid_drain(current_amount_a, current_amount_b, current_time):
# Generate critical alert...
```
### 4. Baseline Calculation
The baseline is calculated using the mean of measurements from the last 24 hours:
```python
def update_baseline(self, current_time: datetime):
cutoff_time = current_time - timedelta(hours=BASELINE_WINDOW_HOURS)
# Baseline for currency A (mean - better for gradual drain detection)
recent_amount_a = [amt for ts, amt in self.amount_a_history
if ts >= cutoff_time and amt > 0]
if recent_amount_a:
self.baseline_amount_a = sum(recent_amount_a) / len(recent_amount_a)
```
**Baseline Requirements:**
- Minimum 10 measurements (`MIN_MEASUREMENTS_FOR_BASELINE`)
- Minimum 5 minutes of data (`MIN_TIME_FOR_BASELINE_MINUTES`)
- Uses mean (not max) for better sensitivity to gradual drains
### 5. Alert Generation
When a liquidity drain is detected, the tool generates a comprehensive alert including:
- **Alert Type**: `liquidity_drop`, `max_amount_decrease`, or `rapid_drain`
- **Severity**: Warning or Critical
- **Pool Information**: Pool ID, address, token pair, DEX protocol
- **Transaction Details**: Transaction hash and timestamp
- **Liquidity Metrics**: Current vs baseline liquidity for both tokens
- **Slippage Data**: Max trade sizes at different slippage levels (10bp, 50bp, 100bp, 200bp, 500bp, 1000bp)
- **Drop Percentages**: Detailed breakdown of liquidity drops
## Sample Alert Output
```
================================================================================
🚨 LIQUIDITY DRAIN ALERT - CRITICAL
================================================================================
Type: liquidity_drop
Pool ID: 0x10bd2f65f40bc8b7ddb6f104c603d022cd8a0ddf_0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2_0xdd9f7920b7c77efa8d1c19e3a7c1151f985f75a6
Pool Address: 0x10bd2f65f40bc8b7ddb6f104c603d022cd8a0ddf
Pair: WETH/ASTRE
DEX: uniswap_v2
Transaction Hash: 0x73470ed71e7b251d0e94078559d3c9005dc14187f55b61ad9435e99feb2341ea
Time: 2026-01-15 13:17:25 UTC
ASTRE dropped 90.4% from baseline
Current State:
Currency A: WETH (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2)
- Current Liquidity: 0.000000 WETH
- Baseline Liquidity: 1.300800 WETH
- Drop: +100.0%
Currency B: ASTRE (0xdd9f7920b7c77efa8d1c19e3a7c1151f985f75a6)
- Current Liquidity: 15,357,735,936.000000 ASTRE
- Baseline Liquidity: 159,272,254,854.947357 ASTRE
- Drop: +90.4%
A->B MaxAmountIn (all slippage levels):
(10bp): Baseline: 0.000654 WETH, Drop: +100.0%
(50bp): Baseline: 0.003282 WETH, Drop: +100.0%
(100bp): Baseline: 0.006592 WETH, Drop: +100.0%
(200bp): Baseline: 0.013285 WETH, Drop: +100.0%
(500bp): Baseline: 0.034013 WETH, Drop: +100.0%
(1000bp): Baseline: 0.070822 WETH, Drop: +100.0%
B->A MaxAmountIn (all slippage levels):
(10bp): Baseline: 80,055,434.984426 ASTRE, Drop: +90.4%
(50bp): Baseline: 399,877,993.949424 ASTRE, Drop: +90.4%
(100bp): Baseline: 799,081,216.317845 ASTRE, Drop: +90.4%
(200bp): Baseline: 1,594,216,258.495066 ASTRE, Drop: +90.4%
(500bp): Baseline: 3,958,883,536.978619 ASTRE, Drop: +90.4%
(1000bp): Baseline: 7,824,577,583.154605 ASTRE, Drop: +90.4%
History Stats:
- Liquidity A measurements: 19
- Liquidity B measurements: 19
- MaxAmountIn measurements: 19
- Baseline window: Last 24 hours
⚠️ VALIDATION NOTES:
- For Uniswap V4: Liquidity amounts are AGGREGATED across all pools
- MaxAmountIn is POOL-SPECIFIC (drops reflect this specific pool's drain)
- Baseline uses last 24 hours of data
- Check transaction hash on Etherscan to verify on-chain events
================================================================================
```
## Project Structure
The project consists of three main files:
1. **`liquidity_drain_detector.py`** (1052 lines): Main detector logic
- Data structures (`PoolState`, `PoolHistory`, `DrainAlert`)
- Detection engine (`LiquidityDrainDetector` class)
- Kafka consumer integration
- Alert formatting and API communication
2. **`detection_config.py`** (55 lines): Configuration class
- All detection thresholds and parameters
- Time windows and filtering options
- Baseline requirements
3. **`api_server.py`** (345 lines): Flask API server
- REST API endpoints for alerts
- Frontend dashboard serving
- Alert storage and filtering
- Configuration management
## Code Walkthrough
### Main Execution Flow
The main function sets up the Kafka consumer and processes messages:
```python
def main():
# Setup logging
logging.basicConfig(level=logging.INFO)
# Initialize detector
detector = LiquidityDrainDetector()
# Configure Kafka consumer
conf = {
'bootstrap.servers': 'rpk0.bitquery.io:9092,rpk1.bitquery.io:9092,rpk2.bitquery.io:9092',
'group.id': f'{config.username}-liquidity-drain-{group_id_suffix}',
'security.protocol': 'SASL_PLAINTEXT',
'sasl.mechanisms': 'SCRAM-SHA-512',
'sasl.username': config.username,
'sasl.password': config.password,
'auto.offset.reset': 'latest',
}
consumer = Consumer(conf)
consumer.subscribe(['eth.dexpools.proto'])
# Main processing loop
while True:
msg = consumer.poll(timeout=1.0)
if msg is None:
continue
# Parse protobuf message
dex_pool_block = dex_pool_block_message_pb2.DexPoolBlockMessage()
dex_pool_block.ParseFromString(msg.value())
# Process each pool event
for pool_event in dex_pool_block.PoolEvents:
alerts = detector.process_pool_update(pool_event)
# Handle alerts
for alert in alerts:
print(format_alert(alert, state, history))
send_alert_to_api(alert, api_url, logger)
```
### Processing Pool Updates
The `process_pool_update` method handles each pool event:
```python
def process_pool_update(self, dex_pool_event) -> List[DrainAlert]:
# Extract pool information
pool = dex_pool_event.Pool
liquidity = dex_pool_event.Liquidity
price_table = dex_pool_event.PoolPriceTable
# For Uniswap V4, use PoolId; for others, use composite key
if protocol_name == 'uniswap_v4':
pool_id = convert_bytes_to_hex(pool.PoolId).lower()
else:
pool_id = f"{pool_address}_{currency_a}_{currency_b}"
# Update pool state with current liquidity and slippage data
state.amount_a = liquidity.AmountCurrencyA # Already human-readable
state.amount_b = liquidity.AmountCurrencyB # Already human-readable
# Process slippage data at multiple levels (10bp, 50bp, 100bp, etc.)
for price_info in price_table.AtoBPrices:
bps = price_info.SlippageBasisPoints
max_in = price_info.MaxAmountIn # Already human-readable
state.slippage_at_bps_a_to_b[bps] = {
'max_amount_in': max_in,
'min_amount_out': price_info.MinAmountOut,
'price': price_info.Price
}
# Add measurement to history
history.add_measurement(amount_a_human, amount_b_human,
max_amount_a_to_b_100bp, max_amount_b_to_a_100bp,
max_amounts_a_to_b_all, max_amounts_b_to_a_all,
current_time)
# Run detection checks if baseline is sufficient
if history.has_sufficient_baseline(current_time):
alerts.extend(self._check_liquidity_drop(...))
alerts.extend(self._check_max_amount_decrease(...))
alerts.extend(self._check_rapid_drain(...))
```
### API Server Endpoints
The Flask API server provides several endpoints for the web dashboard:
#### Get Alerts
```http
GET /api/alerts?pool=| Sr. No. | Timestamp | DEX Name | Price | DEX Address |
|---|---|---|---|---|
| {index + 1 + currentPage * itemsPerPage} | {trade.Block.Time} | {trade.Trade.Dex.ProtocolFamily} | {trade.Trade.Buy.PriceInUSD} | {trade.Trade.Dex.ProgramAddress} |
BalanceUpdates version (stops working 10 August 2026)