mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-24 20:58:05 +00:00
* fix(trading): price tp/sl exits off the trigger price and retry a reverted sell `_monitor_position_until_exit` handed `position.entry_price` to the sell while the `current_price` that had just triggered the exit sat in the same scope, one line up. `PlatformAwareSeller.execute` does not read a price - the `token_price` it receives *is* the slippage floor - so a stop-loss priced off the entry demands more quote asset than the curve can pay and reverts with 6003 `TooLittleSolReceived`, during the very drop the stop-loss exists to escape. On a take-profit the same mistake runs the other way and the floor protects nothing. `current_price` costs no extra RPC call; `_handle_time_based_exit` has nothing fresher and keeps passing the buy price. The `break` also sat outside both branches of `if sell_result.success:`, so the loop exited whether the sell landed or not - contradicting the "Keep monitoring in case sell can be retried" comment directly above it. The seller's `max_retries` covers transaction submission only, so an on-chain revert was never retried and the position was abandoned mid-crash with `is_active=True`. A failed exit sell now retries on the next price check, re-reading the price so the floor tracks the market, bounded by `trade.max_exit_sell_attempts` (default 3, validated to 1..100) so a permanently reverting token cannot pin the bot on one position. The counter resets if the price recovers out of the exit band, and giving up is logged loudly since the tokens are still held. Fixes #189 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(learning-examples): machine-check the tp/sl exit price and retry path Drives the real `_monitor_position_until_exit` with a stub curve manager serving a scripted price series and a stub seller that records the price it is handed. Offline, no network and no funds moved, per the `verify_*` convention. Eight checks: both exit kinds sell at the triggering price, the entry-price floor is arithmetically unpayable on a drop while the trigger-price floor is payable, a reverted sell is retried and a landing retry closes the position, retries stay bounded, a price recovery resets the counter, a successful sell still closes on the first attempt, and the cap comes from `trade.max_exit_sell_attempts` wired through bot_runner and config_loader. Mutation-tested rather than trusted on a green run - reintroducing the stale entry price drops it to 4/8, giving up after one failure to 4/8, and ignoring the config knob to 7/8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: document the tp/sl exit path and its verifier The caller owns the sell's slippage floor, which is not obvious from `PlatformAwareSeller.execute` - it never reads a price, it just turns `token_price` into `min_quote_output`. Records that, why an exit must price off the triggering price, and that the seller's `max_retries` covers submission only so an on-chain revert has to be retried in the monitor loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
399 lines
23 KiB
Markdown
399 lines
23 KiB
Markdown
# Agent guide
|
|
|
|
Solana trading bot for pump.fun and letsbonk.fun. Snipes newly created tokens and exits on a configured strategy. See [README.md](README.md) for setup and configuration; this file covers what an agent needs that the code doesn't make obvious.
|
|
|
|
`AGENTS.md` is a symlink to this file, so Claude Code, Codex, Cursor, and Windsurf all read the same guide.
|
|
|
|
## Ground rules
|
|
|
|
- **Never run a bot with real funds** to test a change. Use `learning-examples/`, or the simulation scripts below, which move no funds.
|
|
- **Never** touch `.env` or print its contents. `SOLANA_PRIVATE_KEY` is a live key.
|
|
- Don't commit anything from `logs/`.
|
|
- Test with a learning example before touching `src/`.
|
|
|
|
## Layout
|
|
|
|
```
|
|
src/ bot source — this dir is the import root (see below)
|
|
learning-examples/ standalone scripts; each runs on its own, no bot config
|
|
bots/ one YAML per bot instance
|
|
idl/ vendored Anchor IDLs
|
|
logs/ {bot_name}_{timestamp}.log
|
|
```
|
|
|
|
**Imports are rooted at `src/`, not at the repo.** `uv pip install -e .` puts
|
|
`src/` itself on `sys.path`, so it is `from core.client import SolanaClient` and
|
|
`from utils.logger import get_logger` — **not** `from src.core...`. Learning
|
|
examples are deliberately self-contained: they import siblings like `pump_v2`
|
|
and `tx_status` as top-level modules and mostly don't import from `src` at all.
|
|
Don't "fix" an example by rewiring it to import the bot.
|
|
|
|
Dependency layers, low to high — don't introduce an upward import:
|
|
|
|
`interfaces` → `utils` → `core` → `platforms` → `monitoring` → `trading` → `bot_runner`
|
|
|
|
`interfaces` is the leaf — it imports nothing internal, and `utils/idl_manager.py`
|
|
imports `interfaces.core`. `geyser` holds only generated stubs and likewise
|
|
imports nothing internal; `cleanup` sits on `core`/`utils` and is pulled in by
|
|
`trading`.
|
|
|
|
Platform differences are resolved through `interfaces/core.py` abstractions
|
|
(`AddressProvider`, curve manager, event parser, instruction builder) and a
|
|
registry in `platforms/__init__.py`. Listeners and the trader are
|
|
platform-agnostic (`Universal*`); anything platform-shaped belongs under
|
|
`platforms/<name>/`.
|
|
|
|
### Naming inside `learning-examples/`
|
|
|
|
- **Directories are kebab-case** (`bonding-curve-progress`, `listen-new-tokens`,
|
|
`copy-trading`). A single-token product name stays one word (`pumpswap`).
|
|
- **Files are snake_case, verb first** — `fetch_price.py`, `decode_from_*.py`,
|
|
`extract_blocksubscribe_transactions.py`, `verify_*.py`, `simulate_*.py`.
|
|
Exceptions are the shared helper modules `pump_v2.py` and `tx_status.py`, which
|
|
are libraries rather than runnable scripts.
|
|
- **RPC and service names are lowercased into one token**, never camelCase:
|
|
`blocksubscribe`, `logsubscribe`, `programsubscribe`, `getaccountinfo`,
|
|
`gettransaction`, `pumpportal`. So `decode_from_gettransaction.py`, not
|
|
`decode_from_getTransaction.py`.
|
|
- Fixtures are `raw_<what>_from_<method>.json` next to the script that reads
|
|
them, under the same rules.
|
|
- `simulate_*` and `verify_*` never move funds — that half of the naming is
|
|
load-bearing and machine-checked. The inverse is **not** true: `live_*` is not
|
|
the only prefix that spends. `manual_*` (including the `pumpswap/` and
|
|
`letsbonk-buy-sell/` ones), `mint_and_buy*` and `cleanup_accounts.py` all
|
|
submit real transactions. Read the module docstring before running anything
|
|
that is not `simulate_*` or `verify_*`.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
uv sync # install runtime deps + the dev group (ruff)
|
|
uv pip install -e . # editable install (required for the imports above)
|
|
pump_bot # run all enabled bots
|
|
uv run src/bot_runner.py # same, without the console script
|
|
```
|
|
|
|
Lint and format **the files you touched**, not the whole tree:
|
|
|
|
```bash
|
|
uv run ruff check --fix <paths> && uv run ruff format <paths>
|
|
```
|
|
|
|
A bare `uv run ruff check` reports ~1700 pre-existing errors across the repo.
|
|
That is the known baseline, not something your change caused — don't try to fix
|
|
it wholesale, and don't read it as a failing build. Just don't add new ones in
|
|
the files you edit.
|
|
|
|
Ruff config lives in `pyproject.toml`: line length 88, double quotes, target
|
|
py311, `E501` ignored. Selected rule families include `ANN` (type annotations),
|
|
`S` (security), `BLE`/`TRY` (exceptions), `C90`/`PL` (complexity), `ERA` (no
|
|
commented-out code). Type-hint public functions, Google-style docstrings, and
|
|
`get_logger(__name__)` for logging.
|
|
|
|
Python 3.11+ (`requires-python = ">=3.11"`, matching ruff's target). Runtime
|
|
deps are declared in `[project.dependencies]`; `ruff` and `grpcio-tools` live in
|
|
`[dependency-groups] dev`, which `uv sync` installs by default. `grpcio-tools`
|
|
is protoc — needed only to regenerate the `geyser_pb2` stubs in
|
|
`src/geyser/generated/` from `src/geyser/proto/`, never at runtime. That is the
|
|
**only** copy: the geyser examples reach it by putting the repo root on
|
|
`sys.path` and importing `src.geyser.generated`. Don't add a second copy under
|
|
`learning-examples/` — the last one drifted out of sync with the protos.
|
|
|
|
### Verifying pump.fun v2 trade instructions
|
|
|
|
```bash
|
|
# Offline: cross-check buy_v2/sell_v2 account layouts, PDA/ATA derivations,
|
|
# instruction encoding and quote-asset config against idl/pump_fun_idl.json
|
|
uv run learning-examples/verify_v2_account_layout.py
|
|
|
|
# Mainnet, no funds moved: simulate buy_v2/sell_v2 for one coin, report CU
|
|
uv run learning-examples/simulate_v2_trades.py <MINT>
|
|
|
|
# Mainnet, no funds moved: run the bot's whole buy path against a fresh coin
|
|
uv run learning-examples/simulate_bot_buy_path.py
|
|
uv run learning-examples/simulate_bot_buy_path.py --no-extreme-fast
|
|
```
|
|
|
|
Run all three after any pump.fun program upgrade. The simulations report
|
|
`unitsConsumed`; use it to retune `get_buy_compute_unit_limit` /
|
|
`get_sell_compute_unit_limit` in `platforms/pumpfun/instruction_builder.py`.
|
|
|
|
### Verifying the listener-to-buy path (issue #170)
|
|
|
|
```bash
|
|
# Offline: bonding curve derived from the mint (payload bondingCurveKey not
|
|
# trusted), unreadable curve skips the buy instead of submitting with guessed
|
|
# accounts, curve+mint read in one slot-consistent batch
|
|
uv run learning-examples/verify_pumpportal_buy_path.py
|
|
|
|
# Offline: extreme_fast_mode stays at ZERO RPC calls between detection and
|
|
# submission for CreateEvent-sourced tokens; pumpportal still refreshes
|
|
uv run learning-examples/verify_extreme_fast_zero_rpc.py
|
|
```
|
|
|
|
Fast listeners (pumpportal especially, but geyser too) can announce a token
|
|
seconds before every node behind a load-balanced RPC endpoint can read its
|
|
accounts — two back-to-back reads on the same endpoint may be served from
|
|
nodes at different slots. `trade.curve_refresh_budget` (seconds, default 2.0)
|
|
bounds the pre-buy curve read in `extreme_fast_mode`; when it expires the token
|
|
is skipped, because a buy built from listener-guessed defaults reverts on-chain
|
|
with `NotAuthorized` (6000), `ConstraintSeeds` (2006) or, on letsbonk,
|
|
`AccountNotInitialized` (3012). The sell path deliberately keeps the opposite
|
|
fallback — proceed with cached values — since skipping a sell strands the
|
|
position.
|
|
|
|
The refresh is skipped entirely — extreme_fast_mode's zero-RPC contract —
|
|
when `TokenInfo.state_from_event` is set, i.e. the listener parsed the
|
|
**CreateEvent** (geyser/logs/blocks), which carries the canonical creator,
|
|
mayhem/cashback flags and quote_mint. Instruction `args.creator` is
|
|
user-supplied and post-2026-04-28 may differ from the canonical `BC.creator`
|
|
(PFEE PDA delegation), so instruction-parsed TokenInfo deliberately does
|
|
**not** set the flag; the geyser parser prefers `meta.log_messages` over
|
|
instruction decoding for exactly this reason. `trade.trust_create_event:
|
|
false` is the escape hatch back to always-refresh. PumpPortal payloads carry
|
|
none of these fields and always refresh. Related pitfall (fixed in #184): the
|
|
IDL instruction decoder used to reject `create_v2` transactions that omit the
|
|
trailing `is_cashback_enabled` OptionBool (a legal wire form), silently
|
|
dropping those coins from the instruction path. It now reports omitted
|
|
trailing option-typed args as unset — `verify_create_v2_optional_args.py`
|
|
machine-checks that, and that mandatory args still fail the decode. The
|
|
log/event path stays preferred for the canonical-creator reason above.
|
|
|
|
### Verifying transaction-status handling
|
|
|
|
```bash
|
|
# Offline: stub checks plus a scan that every example verifies meta.err
|
|
uv run learning-examples/verify_tx_status_checks.py
|
|
|
|
# Adds a mainnet replay of the reverted signatures from issue #175
|
|
uv run learning-examples/verify_tx_status_checks.py --live
|
|
```
|
|
|
|
`confirm_transaction` answers "did this land in a block?", never "did it
|
|
succeed". A landed transaction can have reverted, and RPC reports that only in
|
|
`meta.err`. Reporting success without reading it is issue #175: buys reverting
|
|
with `BuybackFeeRecipientMissing` (6062) printed as confirmed buys.
|
|
|
|
- Examples use `learning-examples/tx_status.py` — `confirm_and_assert` in place
|
|
of a bare `confirm_transaction`, or `assert_transaction_succeeded` after one.
|
|
The verifier above fails the build if a new example skips it.
|
|
- The bot uses `SolanaClient.confirm_transaction`, which folds `meta.err` into
|
|
its return value. **Read the boolean** — discarding it is the same bug.
|
|
- `_get_transaction_result` must send `maxSupportedTransactionVersion: 0` or the
|
|
RPC rejects every versioned (v0) transaction with `-32015`, and a good trade
|
|
reads back as unconfirmed.
|
|
- `build_and_send_transaction` returns a solders `Signature`, not a `str`. A
|
|
`Signature` is not JSON serializable and does not support slicing; a `str` is
|
|
rejected by solana-py's `confirm_transaction`. Normalize at the boundary.
|
|
- `post_rpc` must catch `asyncio.TimeoutError` alongside `aiohttp.ClientError`.
|
|
aiohttp raises the former when the request timeout fires and it is **not** a
|
|
`ClientError`, so leaving it out lets every RPC timeout escape unretried —
|
|
and `str()` on it is empty, so the caller logs a blank reason. A slow
|
|
`getAccountInfo` is enough to take down a whole listener run this way.
|
|
|
|
### Verifying the tp/sl exit path (issue #189)
|
|
|
|
```bash
|
|
# Offline: the exit sell prices off the price that triggered it, a reverted
|
|
# exit sell is retried, and the retry is bounded
|
|
uv run learning-examples/verify_tp_sl_exit_price.py
|
|
```
|
|
|
|
`PlatformAwareSeller.execute` does not read a price — the `token_price` it is
|
|
handed **is** the slippage floor (`min_quote_output = amount * price *
|
|
(1 - slippage)`). So the caller owns the floor's correctness. A tp/sl exit fires
|
|
precisely because price left `entry_price`, so pricing the sell off the entry
|
|
sets a floor the pool cannot pay on a stop-loss and the sell reverts with 6003
|
|
`TooLittleSolReceived` — during the drop the stop-loss exists to escape. On a
|
|
take-profit the same mistake runs the other way and the floor protects nothing.
|
|
`_monitor_position_until_exit` already fetches `current_price` at the top of
|
|
each iteration, so passing it costs no extra RPC call; `_handle_time_based_exit`
|
|
genuinely has nothing fresher and keeps passing the buy price.
|
|
|
|
The seller's `max_retries` covers **transaction submission only**. An on-chain
|
|
revert comes back as `success=False` and is not retried there, so the retry has
|
|
to happen in the monitor loop, where the price is re-read first.
|
|
`trade.max_exit_sell_attempts` (default 3, validated to 1..100) bounds it so a
|
|
token that keeps reverting cannot pin the bot on one position, and the counter
|
|
resets if the price recovers out of the exit band. After the last attempt the
|
|
position is left open and unmonitored — logged loudly, since the tokens are
|
|
still held. Watch the `break`: before #189 it sat outside both branches of
|
|
`if sell_result.success:`, so a failed sell abandoned the position after a
|
|
single try while leaving `is_active=True`.
|
|
|
|
### Listener and decoder pitfalls
|
|
|
|
Each of these was a live bug in `learning-examples/`, all of them invisible
|
|
offline and only visible after a couple of minutes against mainnet.
|
|
|
|
- **A `while True: recv()` loop must break out on `websockets.ConnectionClosed`.**
|
|
Catching it in a broad `except Exception` that only logs makes the next `recv()`
|
|
raise immediately, forever: `listen-new-tokens/compare_listeners.py` produced **13,090,862 error
|
|
lines / 888 MB in 150 s** and never reached its own 30-second report. The outer
|
|
reconnect handler with its `sleep` is unreachable in that shape. A narrow
|
|
`except TimeoutError` or `except json.JSONDecodeError` is fine to swallow —
|
|
those are per-message, not per-connection.
|
|
- **Resolve v0 lookup-table accounts before indexing them.** An instruction's
|
|
account indices can point past `message.account_keys` into the address lookup
|
|
table, which geyser reports in `meta.loaded_writable_addresses` then
|
|
`loaded_readonly_addresses` (that order). Ignoring them crashed the geyser
|
|
example with `IndexError` after ~11 coins in 150 s; resolving them removed the
|
|
crash and brought its detection count level with the WebSocket listeners
|
|
(35 coins each over the same window).
|
|
- **Identify an instruction by its 8-byte discriminator, never by account count.**
|
|
Several pump.fun instructions share a count, so counting mislabels them and
|
|
then prints every account under the wrong name — a real 19-account `create_v2`
|
|
was reported as `claim_cashback`. Note `buy_exact_sol_in` is also 18 accounts
|
|
on chain, same as legacy `buy`.
|
|
- **Walk `meta.innerInstructions`, not just `message.instructions`.** Most trades
|
|
reach the program as a CPI from a router or aggregator: in 40 consecutive
|
|
pump.fun transactions there was **1 top-level** pump instruction against
|
|
**8 inner** ones. Anchor's event-CPI prefix (`e445a52e51cb9a1d`) accounts for a
|
|
good share of the inner instructions; the event's own discriminator follows it.
|
|
- **`getProgramAccounts` over the whole pump program is rejected** by current
|
|
providers: *"Too many accounts requested (10000001 pubkeys) … use
|
|
getProgramAccountsV2 with pagination"*. It still works against pump-amm, which
|
|
is small enough. Don't take that error message as a fix: `getProgramAccountsV2`
|
|
is a provider extension (Helius, Solana Tracker), **not core Agave**, and its
|
|
`limit` is a *scan* budget rather than a result count — a page can legally
|
|
return zero accounts and a non-null `paginationKey`, so one filtered answer over
|
|
the pump program costs ~1000 sequential pages. Reach for a filtered
|
|
subscription instead; see the two `get_graduating_tokens*.py` examples.
|
|
- **Filtered `programSubscribe` on the pump program is the portable way to find
|
|
curves by state.** `dataSize` + `memcmp` are applied server-side, and it is
|
|
accepted even by the public `api.mainnet-beta.solana.com`. `memcmp` only matches
|
|
exact bytes, so it cannot express "reserves below X" — only a handful of fixed
|
|
cutoffs. Treat it as a bandwidth saver and do the real comparison client-side;
|
|
don't assume a threshold is being enforced upstream. Geyser's account filters
|
|
have the same shape and add the slot and signature.
|
|
- **Resolve a curve's mint under Token-2022, not SPL Token.** The curve account
|
|
has no mint field and `["bonding-curve", mint]` is not reversible, so the mint
|
|
comes from the associated bonding curve ATA — which is Token-2022 for every
|
|
`create_v2` coin. `get_token_accounts_by_owner` with the SPL Token program
|
|
returns an empty list for all of them, silently. Verified four for four on live
|
|
curves, each confirmed by re-deriving the curve PDA from the recovered mint.
|
|
- **`SetLoadedAccountsDataSizeLimit` must stay generous: 16 MB, not 512 KB.**
|
|
Verified by simulation on a Token-2022 mint with extensions — 512 KB and 4 MB
|
|
both fail `MaxLoadedAccountsDataSizeExceeded` with `unitsConsumed=0` (never
|
|
executed), while 16 MB reaches the buy instruction and is still 4x under the
|
|
64 MB default. solders has no builder for it; encode `struct.pack("<BI", 4, n)`
|
|
against the compute-budget program.
|
|
|
|
## Pump.fun protocol notes (gotchas)
|
|
|
|
The IDLs under `idl/` are vendored verbatim from `github.com/pump-fun/pump-public-docs`
|
|
(`idl/pump.json` → `pump_fun_idl.json`, `pump_amm.json` → `pump_swap_idl.json`,
|
|
`pump_fees.json`). Refresh them from upstream rather than hand-editing.
|
|
|
|
### Quote assets and the v2 trade instructions (current path)
|
|
|
|
- pump.fun supports quote assets other than SOL. `BondingCurve.quote_mint` is
|
|
`Pubkey::default()` (all zeros) for SOL-paired coins; USDC
|
|
(`EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`) is whitelisted in `Global`.
|
|
**Legacy `buy`/`sell` cannot trade non-SOL-paired coins at all.**
|
|
- The bot uses **`buy_v2` (27 accounts)** and **`sell_v2` (26 accounts)**. Every
|
|
account is mandatory and the order is identical for every coin — SOL or USDC
|
|
paired, mayhem or not, cashback or not. `sell_v2` is `buy_v2` minus
|
|
`global_volume_accumulator`. Layouts live in `_BUY_V2_ACCOUNTS` /
|
|
`_SELL_V2_ACCOUNTS` in `platforms/pumpfun/instruction_builder.py` and are
|
|
machine-checked against the IDL by `learning-examples/verify_v2_account_layout.py`.
|
|
- v2 args carry **no `track_volume` OptionBool** (24-byte data: discriminator +
|
|
two u64). Volume tracking is unconditional now that `user_volume_accumulator`
|
|
is mandatory. `max_sol_cost`/`min_sol_output` are in the **quote mint's** raw
|
|
units — lamports for SOL, 1e-6 for USDC.
|
|
- Even for SOL-paired coins you must pass **wrapped SOL** as `quote_mint`, not
|
|
`Pubkey::default()`. Transfers still happen in native SOL, and the
|
|
`associated_quote_*` accounts are only seed-constrained — do **not** create the
|
|
user's WSOL ATA, it would burn ~0.002 SOL of rent for nothing.
|
|
- Fee recipients: 24 total, in three sets of 8 (`NORMAL_FEE_RECIPIENTS`,
|
|
`RESERVED_FEE_RECIPIENTS` for mayhem coins, `BUYBACK_FEE_RECIPIENTS`). Every
|
|
v2 buy/sell needs a `fee_recipient` **and** a `buyback_fee_recipient`. The set
|
|
is randomized per tx, per pump.fun's guidance on spreading program throughput.
|
|
- `sharing_config` (PDA `["sharing-config", base_mint]`) lives under the **pump
|
|
fees program**, not the pump program. Easy to derive against the wrong program.
|
|
|
|
### BondingCurve account layout
|
|
|
|
- The account is **151 bytes**: 8-byte discriminator, then
|
|
`virtual_token_reserves, virtual_quote_reserves, real_token_reserves,
|
|
real_quote_reserves, token_total_supply` (u64 each), `complete` (1B, offset 48),
|
|
`creator` (32B, offset 49), `is_mayhem_mode` (offset 81),
|
|
`is_cashback_coin` (offset 82), `quote_mint` (32B, offset 83), then 36 reserved
|
|
zero bytes. The documented struct is 115 bytes; the extra 36 are padding.
|
|
- The SOL-named fields were **renamed**: `virtual_sol_reserves` →
|
|
`virtual_quote_reserves`, `real_sol_reserves` → `real_quote_reserves`. The
|
|
curve manager still exposes the old names as aliases, so pre-existing callers
|
|
keep working for SOL-paired coins — but anything doing arithmetic must scale by
|
|
the quote mint's decimals (`quote_units_per_token`), not a hardcoded 1e9.
|
|
- PumpSwap `Pool` gained a trailing **`virtual_quote_reserves: i128`** (16 bytes,
|
|
offset 245). Pool fields end at 261; live accounts are **301 bytes** with
|
|
trailing padding. Quote against **effective** reserves:
|
|
`pool_quote_token_account.amount + virtual_quote_reserves`.
|
|
**Upstream's release note claims it is 0 on all pools — that is out of date.**
|
|
Verified on mainnet: pool `6Bv1JM1deBPe…` carries 17.584505433 SOL of virtual
|
|
reserves against a 148.455 SOL vault, so quoting off the raw vault balance
|
|
under-prices by ~10.6%. It is `i128`, not `u64` — reading 8 bytes happens to
|
|
work only while the high half is zero.
|
|
- pump-amm has **no** `buy_v2`/`sell_v2`. The AMM instruction names are
|
|
unchanged; only the pool layout and quoting moved.
|
|
|
|
### Coin creation
|
|
|
|
- The IDL instruction is `create_v2` (snake_case). 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.
|
|
- **`is_cashback_enabled` can be absent from the wire entirely.** Two mainnet
|
|
`create_v2` instructions carry `0001` and `00` after `creator`: one sends both
|
|
trailing args, the other omits the last. A decoder that reads a fixed number of
|
|
trailing bytes raises `IndexError` on roughly half of all coins. Decode
|
|
trailing args defensively and report a missing one as unset —
|
|
`utils/idl_parser.py` does this for trailing option-typed args since #184
|
|
(`uv run learning-examples/verify_create_v2_optional_args.py` checks it).
|
|
- `create_v2` accounts 1-16 are in the IDL; accounts **17-19 are optional
|
|
remaining accounts** (`quote_mint`, `associated_quote_bonding_curve`,
|
|
`quote_token_program`). All three or none. This is the only way to read a new
|
|
coin's quote asset from the instruction rather than the event. In practice they
|
|
are appended for **SOL-paired coins too**, carrying wrapped SOL — a live
|
|
SOL-paired `create_v2` was observed with 19 accounts and account 17 = WSOL — so
|
|
do not treat a 19-account `create_v2` as proof of a non-SOL quote asset. Read
|
|
`quote_mint` off the curve instead.
|
|
- The **associated bonding curve is an ordinary ATA**, so its address depends on
|
|
which token program owns the mint: Token2022 for `create_v2` coins, SPL Token
|
|
for legacy `create`. Deriving with the wrong program returns a valid-looking
|
|
address that does not exist on chain. Verified: curve `3jJ83ND…` derives to
|
|
`Cd4iC3Jn…` under Token2022 (matches chain) and `AhNzZsBp…` under SPL Token.
|
|
- `extreme_fast_mode` skips the curve-state price fetch. Whether it also reads
|
|
the curve for mayhem/cashback/creator/**quote_mint** depends on provenance
|
|
(see "Verifying the listener-to-buy path" above): CreateEvent-sourced tokens
|
|
(`state_from_event`) trade on the event data with zero RPC calls, while
|
|
pumpportal/incomplete-event tokens refresh from chain — the wrong quote mint
|
|
means spending the wrong balance entirely. Event parsers populate
|
|
`quote_mint` from `CreateEvent` (which gained `quote_mint` and
|
|
`virtual_quote_reserves` as trailing fields).
|
|
|
|
### Legacy instructions (fallback only)
|
|
|
|
Retained behind `PumpFunInstructionBuilder(..., use_legacy_instructions=True)`.
|
|
The IDL under-reports these: `buy` is **18 accounts** on-chain (IDL lists 16) and
|
|
`sell` is **16 non-cashback / 17 cashback** (IDL lists 14). The extras are
|
|
`bonding-curve-v2` (PDA `["bonding-curve-v2", mint]`) followed by a buyback fee
|
|
recipient (mutable); the cashback sell path also inserts
|
|
`user_volume_accumulator` before `bonding-curve-v2`. On the PumpSwap side the
|
|
legacy path needs `pool-v2` (PDA `["pool-v2", base_mint]` under pump-amm) —
|
|
without it pump-amm throws `AnchorError 6023 (Overflow)` after the transfers
|
|
complete, a misleading code for a missing account. Prefer v2 — it is the
|
|
interface pump.fun maintains.
|
|
|
|
## Config notes
|
|
|
|
- Bot YAML supports `${VAR}` interpolation from the file named by `env_file`.
|
|
Actual variable names are `SOLANA_NODE_RPC_ENDPOINT`,
|
|
`SOLANA_NODE_WSS_ENDPOINT`, `SOLANA_PRIVATE_KEY`, `GEYSER_*`.
|
|
- `config_loader.py` validates the platform/listener pairing before startup:
|
|
pump.fun supports `logs`, `blocks`, `geyser`, `pumpportal`; letsbonk.fun
|
|
supports `blocks`, `geyser`, `pumpportal` — **not `logs`**. Adding a listener
|
|
means updating `PLATFORM_LISTENER_COMPATIBILITY` there too.
|
|
- Bots with `separate_process: true` run in their own process. One log file per
|
|
bot instance.
|