chore(pumpswap): post-2026-04-28 cutover + cashback layout (#168)

* chore(pumpswap+docs): post-2026-04-28-cutover PumpSwap unlock + IDL docs

Drops the `INCLUDE_BREAKING_FEE_ACCOUNTS = False` gate in the PumpSwap
learning examples so the +2 breaking-fee accounts (fee recipient
readonly + its quote-mint ATA mutable) are always appended after
`pool-v2`. Mainnet pump-amm rejects this format pre-cutover (verified
6023 Overflow), so this PR is intentionally **draft until 2026-04-28
16:00 UTC** when the cutover happens; mark it ready-for-review and live-
test then.

Also captures the protocol gotchas we learned during this migration:

- README: a new "2026-04-28 program upgrade" section + an explicit note
  that the vendored IDL is incomplete (missing `bonding-curve-v2` and
  `pool-v2` PDAs that the on-chain program actually requires) with
  pointers to cross-check against on-chain txs.
- CLAUDE.md: a new "Pump.fun protocol notes" subsection summarising
  the same gotchas plus the BC/Pool/CreateEvent layout details and the
  extreme_fast_mode gotcha.

Open question (call out at review time, resolve post-cutover):
- BREAKING_FEE_RECIPIENT.md shows PumpSwap cashback account counts of
  27 buy / 26 sell vs 26 / 24 non-cashback — the extra cashback account
  seed/position isn't documented. Need to sample a real successful
  cashback PumpSwap tx after cutover and add the branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pumpswap): wire cashback layout for post-2026-04-28 program upgrade

Cashback PumpSwap pools require extra writable accounts inserted before
pool-v2. Identified layouts by sampling on-chain post-cutover txs:

- buy: insert user_volume_accumulator_quote_ata (27 accounts vs 26)
  ref tx 4JaWdExj…fvjK
- sell: insert user_volume_accumulator_quote_ata + user_volume_accumulator
  (26 accounts vs 24)
  ref tx 4ei1cJV7…NP3

Detect via pool account byte 244 (is_cashback_coin). Adds the standalone
sample_cashback_pumpswap.py used to reverse-engineer the layouts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anton Sauchyk
2026-04-28 16:20:53 +00:00
committed by GitHub
parent 22a0c23450
commit cc85daaeb3
5 changed files with 262 additions and 62 deletions
+9
View File
@@ -62,6 +62,15 @@ ruff check
ruff check --fix
```
## Pump.fun protocol notes (gotchas)
- The vendored IDL at `idl/pump_fun_idl.json` is **incomplete**: it does not list `bonding-curve-v2` (BC trades) or `pool-v2` (PumpSwap trades). Both are required on-chain. `bonding-curve-v2` PDA seed is `["bonding-curve-v2", mint]` under the pump program; `pool-v2` is `["pool-v2", base_mint]` under the pump-amm program. Always cross-check account lists against a recent successful on-chain tx — IDL alone will produce broken code.
- BC `buy` ix is **18 accounts** (post 2026-04-28 upgrade). The trailing account is one of 8 `BREAKING_FEE_RECIPIENTS` (mutable), AFTER `bonding-curve-v2`.
- BC `sell` ix is **16 accounts non-cashback / 17 cashback**. Cashback path inserts `user_volume_accumulator` (PDA seed `["user_volume_accumulator", user]`) BEFORE `bonding-curve-v2`. Detect cashback from byte 82 of the bonding-curve account data, or via the `is_cashback_coin` field returned by the curve manager.
- The BondingCurve account is **83 bytes** (was 81): trailing fields are `is_mayhem_mode: bool` then `is_cashback_coin: bool`. PumpSwap Pool is **245 bytes** with the same `is_cashback_coin` byte at offset 244.
- The IDL instruction is `create_v2` (snake_case). `create_v2` args: `name (str), symbol (str), uri (str), creator (pubkey), is_mayhem_mode (bool), is_cashback_enabled (OptionBool 1B)`. `OptionBool` is a struct wrapping a single bool — serialized as 1 byte, not 2.
- `extreme_fast_mode` skips the curve-state RPC fetch — make sure event parsers populate `is_mayhem_mode` and `is_cashback_coin` on `TokenInfo` from the `CreateEvent` payload (otherwise mayhem coins use the wrong fee_recipient and cashback coins use the wrong sell account count).
## Code Style & Conventions
### Python Style (Ruff Configuration)
+16
View File
@@ -124,6 +124,22 @@ This helps ensure reliable operation within your node provider's rate limits wit
The IDLs under [`idl/`](idl/) are vendored from [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs). To refresh, copy `pump.json`, `pump_amm.json`, `pump_fees.json` from that repo into `pump_fun_idl.json`, `pump_swap_idl.json`, `pump_fees.json` respectively, and reference the upstream commit hash in your commit message.
> **The IDL is incomplete.** It doesn't list two PDAs that the on-chain program actually requires:
> - `bonding-curve-v2` — required on every BC `buy` (18 accounts) and `sell` (16/17 accounts). Seed: `["bonding-curve-v2", mint]` under the pump program.
> - `pool-v2` — required on every PumpSwap `buy`/`sell`. Seed: `["pool-v2", base_mint]` under the pump-amm program. **Without it, pump-amm throws `AnchorError 6023 (Overflow)` after the trade transfers complete** — a misleading error code for a missing-account issue.
>
> Always cross-check your account lists against a recent successful on-chain tx (`getSignaturesForAddress` + `getTransaction`) before trusting the IDL.
## 2026-04-28 program upgrade
Pump.fun shipped a breaking program upgrade on **2026-04-28 16:00 UTC** ([BREAKING_FEE_RECIPIENT.md](https://github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md)). The bot is updated for it:
- BC `buy` ix is now **18 accounts** (was 17). Trailing account is one of 8 `BREAKING_FEE_RECIPIENTS` (mutable), AFTER `bonding-curve-v2`.
- BC `sell` ix is now **16 accounts non-cashback / 17 cashback** (was 15/16). Same trailing fee recipient.
- PumpSwap `buy`/`sell` get **+2 accounts** appended after `pool-v2`: a fee recipient (readonly) and its quote-mint ATA (mutable). Counts: buy = 26 non-cashback / 27 cashback; sell = 24 / 26. Cashback pools insert `user_volume_accumulator_quote_ata` (writable) BEFORE `pool-v2` on buys; sells insert both that ATA and `user_volume_accumulator` (both writable) BEFORE `pool-v2`. Detect cashback via pool data byte 244.
The 8 fee recipients are randomized per tx in code (per pump.fun's recommendation to spread program-tx throughput).
## Changelog
Quick note on a couple on a few new scripts in `/learning-examples`:
@@ -100,6 +100,7 @@ BREAKING_FEE_RECIPIENTS = [
POOL_DISCRIMINATOR_SIZE = 8
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
POOL_IS_CASHBACK_OFFSET = 244 # Where is_cashback_coin flag is stored
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
# GlobalConfig structure offsets
@@ -326,18 +327,11 @@ async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
async def get_pumpswap_fee_recipients(
client: AsyncClient, pool: Pubkey
) -> tuple[Pubkey, Pubkey]:
"""Determine the correct fee recipient based on pool's mayhem mode status.
This function checks if mayhem mode is enabled for the pool and returns
the appropriate fee recipient and their WSOL token account.
Args:
client: Solana RPC client
pool: Address of the AMM pool
) -> tuple[Pubkey, Pubkey, bool]:
"""Determine the correct fee recipient and whether the pool is cashback.
Returns:
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
Tuple of (fee_recipient_pubkey, fee_recipient_token_account, is_cashback)
"""
response = await client.get_account_info(pool, encoding="base64")
if not response.value or not response.value.data:
@@ -346,23 +340,23 @@ async def get_pumpswap_fee_recipients(
pool_data = response.value.data
# Check if mayhem mode flag exists and is enabled
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
pool_data[POOL_MAYHEM_MODE_OFFSET]
)
is_cashback = len(pool_data) > POOL_IS_CASHBACK_OFFSET and bool(
pool_data[POOL_IS_CASHBACK_OFFSET]
)
# Select appropriate fee recipient
if is_mayhem_mode:
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
else:
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
# Get the fee recipient's WSOL token account
fee_recipient_token_account = get_associated_token_address(
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
return (fee_recipient, fee_recipient_token_account)
return (fee_recipient, fee_recipient_token_account, is_cashback)
# ============================================================================
@@ -483,9 +477,16 @@ async def buy_pump_swap(
global_volume_accumulator = find_global_volume_accumulator()
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
# Get fee recipient based on mayhem mode
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
client, market
# Get fee recipient based on mayhem mode + detect cashback pool
(
fee_recipient,
fee_recipient_token_account,
is_cashback,
) = await get_pumpswap_fee_recipients(client, market)
# WSOL ATA of user_volume_accumulator — only required for cashback pools.
user_volume_accumulator_quote_ata = get_associated_token_address(
user_volume_accumulator, SOL, SYSTEM_TOKEN_PROGRAM
)
# Build account list for buy instruction
@@ -528,23 +529,34 @@ async def buy_pump_swap(
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False),
]
# 2 new accounts required from 2026-04-28 16:00 UTC pump-swap program upgrade,
# AFTER pool-v2: breaking-upgrade fee recipient (readonly, second-to-last) and
# its quote-mint ATA (mutable, last). Set to True to opt in.
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
INCLUDE_BREAKING_FEE_ACCOUNTS = False # flip True after 2026-04-28 16:00 UTC
if INCLUDE_BREAKING_FEE_ACCOUNTS:
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
breaking_fee_quote_ata = get_associated_token_address(
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
# Cashback pools require user_volume_accumulator_quote_ata (writable) BEFORE
# pool-v2. Confirmed against on-chain post-cutover cashback buy
# (sig 4JaWdExj6zzU3aGNWqNFhmtCyhbjRU3zLrsA3vASGu9krQrLjAfbBygP9i7yXmruSbuYn4StgMdMFBi22oQCfvjK).
if is_cashback:
accounts.append(
AccountMeta(
pubkey=user_volume_accumulator_quote_ata,
is_signer=False,
is_writable=True,
)
)
accounts.extend([
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
])
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
accounts.append(
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False)
)
# 2 accounts required by the 2026-04-28 pump-swap upgrade, appended AFTER
# pool-v2: breaking-fee recipient (readonly) + its quote-mint ATA (mutable).
# Buy counts: 26 non-cashback / 27 cashback.
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
breaking_fee_quote_ata = get_associated_token_address(
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
accounts.extend([
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
])
# Instruction data format:
# discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte)
@@ -92,6 +92,7 @@ BREAKING_FEE_RECIPIENTS = [
POOL_DISCRIMINATOR_SIZE = 8
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
POOL_IS_CASHBACK_OFFSET = 244 # Where is_cashback_coin flag is stored
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
# GlobalConfig structure offsets
@@ -246,6 +247,16 @@ def find_pool_v2(base_mint: Pubkey) -> Pubkey:
return derived_address
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
"""Derive the per-user volume accumulator PDA (pump-amm). Required for
cashback-pool sells (along with its WSOL ATA)."""
derived_address, _ = Pubkey.find_program_address(
[b"user_volume_accumulator", bytes(user)],
PUMP_AMM_PROGRAM_ID,
)
return derived_address
# ============================================================================
# Mayhem Mode Fee Handling
# ============================================================================
@@ -279,18 +290,11 @@ async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
async def get_pumpswap_fee_recipients(
client: AsyncClient, pool: Pubkey
) -> tuple[Pubkey, Pubkey]:
"""Determine the correct fee recipient based on pool's mayhem mode status.
This function checks if mayhem mode is enabled for the pool and returns
the appropriate fee recipient and their WSOL token account.
Args:
client: Solana RPC client
pool: Address of the AMM pool
) -> tuple[Pubkey, Pubkey, bool]:
"""Determine fee recipient + whether the pool is cashback.
Returns:
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
Tuple of (fee_recipient_pubkey, fee_recipient_token_account, is_cashback)
"""
response = await client.get_account_info(pool, encoding="base64")
if not response.value or not response.value.data:
@@ -299,12 +303,13 @@ async def get_pumpswap_fee_recipients(
pool_data = response.value.data
# Check if mayhem mode flag exists and is enabled
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
pool_data[POOL_MAYHEM_MODE_OFFSET]
)
is_cashback = len(pool_data) > POOL_IS_CASHBACK_OFFSET and bool(
pool_data[POOL_IS_CASHBACK_OFFSET]
)
# Select appropriate fee recipient
if is_mayhem_mode:
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
else:
@@ -315,7 +320,7 @@ async def get_pumpswap_fee_recipients(
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
return (fee_recipient, fee_recipient_token_account)
return (fee_recipient, fee_recipient_token_account, is_cashback)
# ============================================================================
@@ -475,9 +480,18 @@ async def sell_pump_swap(
print(f"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
# Get fee recipient based on mayhem mode
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
client, market
# Get fee recipient based on mayhem mode + detect cashback pool
(
fee_recipient,
fee_recipient_token_account,
is_cashback,
) = await get_pumpswap_fee_recipients(client, market)
# Cashback-pool sells append two writable accounts (user_volume_accumulator
# quote ATA, then the accumulator PDA itself) BEFORE pool-v2.
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
user_volume_accumulator_quote_ata = get_associated_token_address(
user_volume_accumulator, SOL, SYSTEM_TOKEN_PROGRAM
)
# Build account list for sell instruction
@@ -516,23 +530,38 @@ async def sell_pump_swap(
),
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False),
]
# 2 new accounts required from 2026-04-28 16:00 UTC pump-swap program upgrade,
# AFTER pool-v2: breaking-upgrade fee recipient (readonly, second-to-last) and
# its quote-mint ATA (mutable, last). Set to True to opt in.
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
INCLUDE_BREAKING_FEE_ACCOUNTS = False # flip True after 2026-04-28 16:00 UTC
if INCLUDE_BREAKING_FEE_ACCOUNTS:
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
breaking_fee_quote_ata = get_associated_token_address(
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
# Cashback pools require user_volume_accumulator_quote_ata + the accumulator
# PDA itself (both writable) BEFORE pool-v2. Confirmed against on-chain
# post-cutover cashback sell
# (sig 4ei1cJV7uaENJeb5p8prVKiTApTouTh94r9HqPNbj7oJH52X8mEiXhrVNKUgtB9WeZB8jZANnmuSkdTuJ59y8NP3).
if is_cashback:
accounts.extend([
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
AccountMeta(
pubkey=user_volume_accumulator_quote_ata,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=user_volume_accumulator, is_signer=False, is_writable=True
),
])
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
accounts.append(
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False)
)
# 2 accounts required by the 2026-04-28 pump-swap upgrade, appended AFTER
# pool-v2: breaking-fee recipient (readonly) + its quote-mint ATA (mutable).
# Sell counts: 24 non-cashback / 26 cashback.
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
breaking_fee_quote_ata = get_associated_token_address(
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
)
accounts.extend([
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
])
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
# All integers are little-endian (<)
@@ -0,0 +1,134 @@
"""Post-cutover sampler: find a successful cashback PumpSwap buy/sell on mainnet.
Goal: identify the seed/position of the +1 account that the program requires for
cashback pools (27-account buy / 26-account sell vs 26 / 24 non-cashback).
Strategy:
1. Pull recent signatures for pAMM program.
2. For each tx, fetch full tx, find the pAMM buy/sell ix.
3. Resolve the pool account from the ix; fetch its data; check byte 244
(is_cashback_coin) — only proceed if it's 1.
4. Print the full account list with counts so we can diff against the known
26/24 non-cashback layout in manual_buy/sell_pumpswap.py.
Usage:
uv run learning-examples/pumpswap/sample_cashback_pumpswap.py [LIMIT]
Env: SOLANA_NODE_RPC_ENDPOINT (defaults to public mainnet)
"""
import asyncio
import os
import sys
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey
from solders.signature import Signature
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea")
SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad")
# Pool layout: byte 244 = is_cashback_coin (per CLAUDE.md / PR #167 notes).
POOL_IS_CASHBACK_OFFSET = 244
RPC = os.environ.get(
"SOLANA_NODE_RPC_ENDPOINT", "https://api.mainnet-beta.solana.com"
)
async def is_cashback_pool(client: AsyncClient, pool: Pubkey) -> bool | None:
resp = await client.get_account_info(pool, encoding="base64")
if resp.value is None:
return None
data = resp.value.data
if len(data) <= POOL_IS_CASHBACK_OFFSET:
return False
return data[POOL_IS_CASHBACK_OFFSET] == 1
def classify_ix(ix_data: bytes) -> str | None:
if ix_data.startswith(BUY_DISCRIMINATOR):
return "buy"
if ix_data.startswith(SELL_DISCRIMINATOR):
return "sell"
return None
async def inspect_tx(client: AsyncClient, sig: Signature) -> dict | None:
"""Return diagnostic dict if this tx contains a cashback buy/sell."""
resp = await client.get_transaction(
sig, encoding="base64", max_supported_transaction_version=0
)
if resp.value is None or resp.value.transaction.meta is None:
return None
if resp.value.transaction.meta.err is not None:
return None # only successful txs
tx = resp.value.transaction.transaction
msg = tx.message
account_keys = list(msg.account_keys)
# include loaded addresses from ALTs
loaded = resp.value.transaction.meta.loaded_addresses
if loaded is not None:
account_keys.extend(loaded.writable)
account_keys.extend(loaded.readonly)
for ix in msg.instructions:
program_id = account_keys[ix.program_id_index]
if program_id != PUMP_AMM_PROGRAM_ID:
continue
kind = classify_ix(bytes(ix.data))
if kind is None:
continue
# PumpSwap convention: account index 0 of the ix is the pool.
if not ix.accounts:
continue
pool = account_keys[ix.accounts[0]]
cashback = await is_cashback_pool(client, pool)
if not cashback:
continue
return {
"sig": str(sig),
"kind": kind,
"pool": str(pool),
"n_accounts": len(ix.accounts),
"accounts": [str(account_keys[i]) for i in ix.accounts],
}
return None
async def main(limit: int = 200) -> None:
async with AsyncClient(RPC) as client:
print(f"Sampling up to {limit} recent pAMM signatures from {RPC}")
sigs_resp = await client.get_signatures_for_address(
PUMP_AMM_PROGRAM_ID, limit=limit
)
sigs = [s.signature for s in sigs_resp.value if s.err is None]
print(f" got {len(sigs)} successful signatures")
for sig in sigs:
try:
hit = await inspect_tx(client, sig)
except (ValueError, RuntimeError) as e:
print(f" [skip] {sig}: {e}")
continue
if hit is None:
continue
print()
print(f"=== CASHBACK {hit['kind'].upper()} ===")
print(f" sig: {hit['sig']}")
print(f" pool: {hit['pool']}")
print(f" count: {hit['n_accounts']} accounts")
for i, a in enumerate(hit["accounts"]):
print(f" [{i:2d}] {a}")
return
print("No cashback PumpSwap buy/sell found in window.")
if __name__ == "__main__":
n = int(sys.argv[1]) if len(sys.argv) > 1 else 200
asyncio.run(main(n))