letsbonk.fun and StonkFun now share one LaunchLab implementation that prices
from real reserves, trades any quote mint, spends exactly the buy amount, and
accounts for the curve fee and Token-2022 transfer fees. Cleanup harvests
withheld fees before closing an account.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cookbook): cover every create_v2 coin parameter in the launch scripts
Both create scripts now build create_v2 through pumpfun_instructions_v2
instead of each carrying their own copy of the discriminators, PDAs and
account list.
- A holder-reward coin's curve creator is derived rather than assumed, so
the opening buy finds the matching creator vault instead of reverting.
- create_v2's three trailing arguments are settable independently. None
omits an argument, a value sends it, so all four wire forms are
reachable and an explicit default is distinct from an omission.
- create_v2 carries all four remaining accounts, quote_control included.
Without them the program ignores creator_fee_bps.
- --creator, --creator-fee-bps and --quote-mint added. A creator fee on a
SOL-paired coin warns, because the program stores zero for it; the
create-and-buy script refuses one outright before sending anything.
- The opening buy is sized from Global's reserves and fee rates.
- Mayhem mode is off by default.
Removes the fee-recipient lookup whose result was discarded and the buy
wrapper whose parameters were unused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(regression): verify the launch path's create_v2 arguments
Eight offline checks: the substituted creator on a holder-reward coin,
every committed create_v2 fixture round-tripping through the trailing
argument encoder, all four wire forms being reachable, --creator reaching
the wire, the opening buy following Global rather than constants, the
Global decoder stopping cleanly on a short buffer, and the four quote
accounts being present with the ATA under the quote mint's own program.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: correct create_v2's remaining accounts and the creator-fee rule
create_v2 takes four remaining accounts, not three; the fourth is the
QuoteControl PDA. Omitting them is accepted and yields a SOL-paired coin,
which is also how a creator fee goes missing: the program applies
creator_fee_bps only to a coin priced in something other than SOL, and
otherwise stores zero without an error.
Also records that solders documents TransactionConfig.priority_fee as
micro-lamports while SIMD-0385 defines the field as total lamports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cookbook): price a new coin in an asset other than SOL
--quote-mint on both create scripts, checked against both registries that
admit a quote asset. QuoteControl carries most of them with a per-mint
opening reserve; Global.whitelisted_quote_mints carries USDC, which
QuoteControl does not list, so consulting either alone refuses a mint the
program accepts. A mint neither admits is refused rather than priced from
a default, since the opening reserve spans orders of magnitude.
The quote mint's token program and decimals are resolved from chain, and
--amount is denominated in the quote asset rather than in SOL. A paused
mint is refused before anything is sent, and a scaled-UI mint says so,
because amounts here are raw units.
A creator fee now works, since pump.fun applies one only to a coin priced
in something other than SOL. On a SOL-paired coin it is refused instead of
being accepted and stored as zero.
The create-and-buy script requires the wallet to already hold the quote
asset; it does not acquire one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cookbook): launch a coin in one v1 transaction
Create, extend, the buyer's token account and the buy now travel under a
single signature, so nobody can buy between the create and the opening
buy. They were split because the two together are 1479 bytes against the
1232 a v0 transaction is held to; SIMD-0296 raised the limit to 4096 for
v1 and the combined message uses about a third of it.
v1 has no address lookup tables -- the larger limit exists so the whole
account list goes inline -- and it carries the compute budget in the
message header, so the script sends no ComputeBudget instructions. The
priority fee is now one total in lamports rather than micro-lamports per
compute unit; the constant is converted, not renamed.
The two-transaction path is removed rather than kept behind a flag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): state the loaded-accounts data size limit on the v1 launch
A v1 message carries this limit in its header, and left unset it is zero
rather than the network default, so the runtime rejects the transaction
for exceeding it. The launch skips preflight, so the rejection never
reached the caller: send returned a signature that never landed.
Measured against a forked mainnet, the launch is rejected at 128 KiB and
lands at 256 KiB. The limit is set to 1 MiB, which leaves room for a quote
mint whose accounts are larger, and the verifier fails if it is dropped or
set at or below the measured failing size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cookbook): offer the launch as a v0 or a v1 transaction
The Solana transaction version changes what a launch can be, so it is the
script that changes rather than a flag on one of them.
_txv1 sends create and buy under a single signature, so the coin is bought
by the transaction that creates it. _txv0 sends them as two transactions,
because together they do not fit what a v0 transaction can carry, and
anyone watching the create can buy in the gap. Use it when the endpoint
will not take a v1 send.
The coin is identical either way: quote asset, creator fee, holder rewards
and mayhem mode all behave the same.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(cookbook): drop the instruction version from script names
The cookbook ships one form of each pump.fun action, so `_v2` in a
basename distinguished nothing — and it read as a Solana transaction
version, which is a different thing and now appears in the same names.
`pumpfun_buy_token_exact_sol_in.py` already went without it.
Every trade script loses the suffix, the shared module becomes
`pumpfun_instructions.py`, and the retired path is named for what it is
rather than for the instruction set it uses. The instruction each script
builds is named in its docstring, where it cannot be mistaken for a
transaction version. `txv0`/`txv1` is now the only version marker in any
filename, and it is the one the reader chooses between.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): refuse mayhem mode paired with a non-SOL quote asset
The program rejects the pairing with MayhemModeQuoteMintNotAllowed (6071),
so a mayhem coin is always SOL-paired — and therefore can never carry a
creator fee, which needs a non-SOL quote. Both create scripts now refuse
the combination before sending, rather than letting it land as a reverted
transaction that has already cost fees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): size the v1 loaded-accounts limit for what mainnet loads
The limit counts the executable data of every program a transaction
touches, not just the accounts it names, and the pump program's own code
is the bulk of it. The previous figure was set against a local fork, which
does not charge for executables; on mainnet the launch was rejected with
MaxLoadedAccountsDataSizeExceeded.
Set to the network default a v0 transaction gets when nothing narrows it,
which is the behaviour being reproduced. The verifier now holds the
mainnet figure as its floor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): a creator fee needs a QuoteControl quote mint, not just a non-SOL one
Measured on mainnet: the fee is applied on a coin priced in a mint
QuoteControl admits, whether that mint is SPL Token or Token-2022, and is
not applied on wrapped SOL or on USDC — which the older Global whitelist
carries and QuoteControl does not. The deciding property is registry
membership, so the previous check let a USDC-priced coin through and the
program stored zero without an error.
Also stops the sell script calling a price "SOL" when the curve is priced
in something else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cookbook): offer the create-only script as a v0 or a v1 transaction
Same split as the create-and-buy pair, for the same reason: the Solana
transaction version is the reader's choice, so it is the script that
changes rather than a flag. _txv1 carries the compute budget in the
message header and sends no ComputeBudget instructions; _txv0 sends them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): refuse a paused quote mint on the create-and-buy path too
The check only ran in the create-only scripts, so a launch that also buys
could reach the chain with a quote mint whose transfers are paused. On the
two-transaction path that leaves a created coin nobody bought, after the
create has already cost rent.
Also corrects the v1 script's run docstring, which still described two
transactions and a spend denominated in SOL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): say --amount is in whole quote units, not raw ones
The scaled-UI notice told the reader the opposite of what the scripts do:
--amount is multiplied by the mint's decimals, so it is a whole-token
figure. A reader who followed the notice would have overspent by a factor
of the decimals.
Reworded so the multiplier is described as what it is — a display scale a
wallet applies — rather than as something the amount has to account for.
The create-only scripts carried the same wording without even taking an
amount.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(monitoring,platforms,tools): retire the pumpportal listener
Remove the third-party feed from the bot, the cookbook and the listener
race. It sampled rather than covered the chain, and was the one listener
that could never set state_from_event, so every buy paid the pre-buy
curve read.
Rescope the two buy-path verifiers that used it as a stand-in for a
TokenInfo without state_from_event.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* style(trading): rewrap a docstring paragraph
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
shreds is one of the bot's listeners but was missing from the comparison. It
subscribes on the mint-authority PDA rather than the program id, walks top-level
instructions only, and resolves lookup-table accounts off the update itself,
since a pre-execution stream carries no meta.
The window is now a --duration flag (default 60s) rather than a module constant.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(monitoring): add shreds listener for pre-execution token detection
Adds a `shreds` listener backed by geyser's SubscribeDeshred, which delivers
transactions when entries form from shreds, before execution. pump.fun only;
bots/bot-sniper-5-shreds.yaml ships disabled.
With no TransactionStatusMeta there is no CreateEvent, so the listener decodes
the create instruction and resolves what the buy needs itself. It marks its
tokens state_from_event, because there is no curve to read before the create
executes: trading on the instruction or not trading at all are the only
options.
Two fixes to the shared instruction-decode path, which geyser and blocks also
reach when logs are absent:
- `user` is account 5 on create_v2, not 7. create_v2 drops the metaplex
accounts legacy create carries, so index 7 is the token program and every
address derived from the wallet was wrong.
- A holder-reward coin's creator is PDA(["holder-rewards", mint]). The program
ignores args.creator on those coins and writes that PDA into
BondingCurve.creator, so creator_vault must derive from it or the buy is
rejected on a seeds constraint.
Subscribes on the mint authority rather than the program id: it appears in
create/create_v2 and in no trade instruction, which keeps the stream to
creations instead of every pump.fun transaction.
Verified live on mainnet with extreme_fast_mode on and off and both the
time_based and tp_sl exits, each ending with the wallet holding SOL only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: drop reported measurement statistics from prose
Removes sample counts, traffic percentages, latency percentiles, window
durations and "measured on <date>" claims from docstrings, comments, docs and
bot YAML. The fact each measurement supported is kept and stated plainly.
Numbers that are load-bearing rather than reporting stay: protocol and packet
limits, the CU figures used to tune compute budgets, and dates pinning on-chain
or dependency upgrades.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(tests): move the regression verifiers into tests/regression
verify_*.py are not standalone examples: each imports src/, stubs the RPC
client, and asserts one previously fixed bug stays fixed. Their checks are
unchanged; PROJECT_ROOT, the usage lines and the bonk fixture lookup are
adjusted for the new depth. run_all.py runs the set and reports failures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(tools): move the dev harness and listener benchmarks into tools/
simulate_*, live_*, both compare_* scripts and the account cleanup import
src/ and several spend real funds — they exercise the bot rather than teach
anything. compare_listeners.py reaches the repo root one level higher than
before; nothing else changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(cookbook): reorganise learning-examples into cookbook/
The directory is a reference collection — one runnable script per action — but
nothing said so: no index, scripts grouped by the transport they happened to
use, fixtures loose at the root.
Now grouped by platform, then by what you are doing:
pumpfun/{listen,read,trade,graduation,decode}, pumpswap, letsbonk, legacy.
Fixtures sit beside the script that reads them, manual_sell.py becomes
sell_token.py, and the legacy create example moves to legacy/ and says why.
pump_v2.py and tx_status.py stay at the cookbook root as the only two shared
helpers; subdirectories add that root to sys.path before importing them.
cookbook/README.md indexes every script and marks the ones that spend funds.
Also fixes create_v2's account list: the five mayhem accounts are mandatory
whatever is_mayhem_mode says, so mint_and_buy_v2.py could never have worked
with mayhem off. 11 accounts fails AnchorError 3005 (AccountNotEnoughKeys) on
sol_vault, 16 succeeds with the argument either way — simulateTransaction
against mainnet. Seven scripts gain a module docstring they were missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(cookbook): add buy, create, balance and status examples
buy_token.py <MINT> buy_v2 against one coin; --dry-run simulates
create_token.py create_v2 on its own
get_balances.py SOL and tokens across both token programs
check_tx_status.py <SIG> success, reverted, or not visible yet
Buying previously meant manual_buy.py, which waits for the next coin created
anywhere before it trades — there was no way to buy a coin you already know.
Creating one meant bundling a buy with it, and the transaction-status helper
was a library with no entry point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(cookbook): move pump_v2 next to the scripts that use it
pump_v2.py holds the buy_v2/sell_v2 account layouts, and only the pump.fun
trade scripts import it — it sat at the cookbook root because tx_status.py
does, not because anything outside pumpfun/trade/ needed it. The five callers
now pick it up as a plain sibling; legacy/ and the layout verifier reach into
that directory. tx_status.py stays at the root, where all four platforms use it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(cookbook): name every script protocol_verb_noun_variant
manual_buy.py did not just buy — it waited for the next coin created anywhere,
then bought, so the verb was wrong. Protocol was encoded three different ways:
directory, suffix, or not at all. And three scripts sat under pumpfun/ that have
nothing to do with pump.fun.
Filenames are now <protocol>_<verb>_<noun>[_<variant>].py, so a basename says
which chain, what it does and which instruction version without opening it:
manual_buy.py -> pumpfun_snipe_token_blocksubscribe.py
manual_buy_geyser.py -> pumpfun_snipe_token_geyser.py
buy_token.py -> pumpfun_buy_token_v2.py
mint_and_buy_v2.py -> pumpfun_create_and_buy_token_v2.py
manual_buy_pumpswap.py -> pumpswap_buy_token.py
manual_buy_exact_in.py -> letsbonk_buy_token_exact_in.py
legacy/mint_and_buy.py -> legacy/pumpfun_create_and_buy_token_v1.py
The sniper was named for logsSubscribe in an earlier draft; it sends
blockSubscribe, and the name now says so.
Balances, transaction status and the Anchor discriminator calculator are not
pump.fun-specific and move to a new cookbook/solana/. The two non-runnable
helpers take no verb — pump_v2.py becomes pumpfun_instructions_v2.py beside its
callers, tx_status.py becomes solana_transaction_status.py in solana/. Both are
imported under a short alias so the 106 call sites stay readable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cookbook): resolve a coin's quote mint before pricing it
quote_units() defaulted to 9 decimals for an unresolved mint, and every trade
script called it before resolve_quote_token_program() warmed the decimals
cache. pump.fun's QuoteControl registry admits mints at 6, 8 and 9 decimals —
79 of the 170 admitted are tokenized equities, 8 decimals for xStocks and 6 for
Backpack Securities — so those coins were sized against the wrong power of ten.
The error compounds rather than cancels: price_per_token() divides by the same
wrong unit, so the price reads low by the factor, the token amount comes out
high by it, and the inflated cap authorises the overspend instead of catching
it. 10x on an xStock pair, 1000x on a Backpack one.
This is not hypothetical. A 75s sample of live curve updates on 2026-09-22
caught 8 stock-paired curves out of 124 — IONQ across 5 of them, plus QUBT,
PLTRx and COPX.
quote_units() now raises for an unresolved mint instead of guessing, the four
trade paths resolve before they price, and pumpfun_read_price.py reads decimals
from chain rather than from a two-entry table. The geyser sniper never resolved
at all: it also passed SPL Token as the quote token program, which fails the
account constraints for any Token-2022 quote, and created no quote ATA.
verify_quote_decimals_resolved.py checks the fail-closed contract, that
resolution costs no extra RPC call, and that no function prices before it
resolves — confirmed to fail when the ordering is put back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* feat(cookbook): read the quote-mint registry and Token-2022 mint state
Two reads that were unreachable without knowing an undocumented PDA seed.
pumpfun_read_quote_mints.py decodes QuoteControl (PDA ["quote-control"]), the
second quote-mint registry added in the 2026-09-15 upgrade. Reading only
Global.whitelisted_quote_mints under-reports: a coin can be paired with a mint
Global has never heard of. 170 admitted today, 79 of them tokenized equities,
each with the initial virtual quote reserves that set a new curve's opening
price. --stocks narrows to the equities, identified by the Token-2022 extension
set create_v2 requires of them rather than by a name lookup.
solana_read_token2022_mint.py reads the extensions that change what a balance
means: scaled UI amount, pausable, permanent delegate, transfer hook, default
account state. It exists mainly for one trap — the RPC returns both `multiplier`
and `newMultiplier` with an effective timestamp, and once that timestamp passes
the field named `multiplier` is the stale one. On AAPLx it passed on 2026-08-08,
so the obvious field has been wrong for six weeks. This compares against the
clock and says which is in force.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* feat(cookbook): add the exact-quote-in buy
buy_exact_quote_in_v2 was uncovered. It is the mirror of buy_v2 — buy_v2 fixes
the tokens received and caps the spend, this fixes the spend and floors the
tokens — and it takes the same 27 accounts in the same order, so it reuses
build_v2_accounts and differs only in discriminator and arguments.
Worth having because pinning the spend is the natural direction once a coin is
priced in something other than SOL: "spend exactly one AAPLx" is something you
can mean, where a token count is a number derived from a price that moved while
you derived it.
Verified by simulateTransaction against a live COPX-paired coin: the program
executes the full buy and stops at TransferChecked with insufficient funds,
which is as far as a wallet holding none of the quote asset can go.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* feat(cookbook): add the exact-SOL-in buy
buy_exact_sol_in was uncovered. SOL only — it pre-dates non-SOL quote assets and
carries no quote accounts, so the script checks the curve and points at the
exact-quote version for anything else rather than failing obscurely.
Its account list is the reason to have it. The IDL lists 16; the program needs
18, the extras being the bonding-curve-v2 PDA and a buyback fee recipient.
Determined by simulation rather than assumed: 16 accounts fails AnchorError 6062
(BuybackFeeRecipientMissing), 18 simulates clean at 96k CU. The v2 instructions
are complete in the IDL and this family is not, which is worth a newcomer
knowing before they trust the file.
The docstring carries the IDL's own fee arithmetic, since fees come out of the
amount you name rather than on top of it, and the printed estimate ignores that
split while min_tokens_out is what the program enforces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* feat(cookbook): add the creator-fee and cashback payouts
Two payout paths that had no script. Both take no arguments and, per the IDL,
no signer — the funds can only reach the wallet they already belong to, so
either can be run for anyone, which --creator and --user expose.
collect_creator_fee_v2 sweeps a creator vault. The vault is per creator and per
quote asset, so a creator whose coins are priced in several assets collects each
separately; --quote picks which.
claim_cashback_v2 pays out an accrued cashback balance. create_v2 has rejected
new cashback coins since 2026-09-15 (error 6082), but coins minted before that
keep accruing and stay claimable, which is why the cashback handling elsewhere
in this repo is not dead code.
Both verified by simulateTransaction against mainnet: 10 accounts each, 8 data
bytes, no error.
Also repairs two cookbook README rows that the earlier rename mangled —
pumpswap_buy_token.py had been rewritten to pumpswap_pumpfun_buy_token_v2.py
because its name contains the string that rename matched on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* refactor(cookbook): take every script's input on the command line
A reference collection only works if you can run a script against your own coin
without editing it. Several carried the coin as a module-level constant instead:
TOKEN_MINT = Pubkey.from_string(sys.argv[1] if len(sys.argv) > 1 else "...")
which reads as optional but raises at import, so running the script the way its
own usage line described it died on ValueError: Invalid Base58 string before
printing anything. Nine scripts did this. Four more took their amount and
slippage from environment variables that no usage line mentioned.
Every script now builds an ArgumentParser in main(): required values are
positionals, tunables are --options, and the literals live in DEFAULT_* constants
that supply the defaults. --help works on all 32 that take input; the seven
listeners take none and are exempt. The decode scripts keep defaulting to the
fixture beside them, so they still run bare.
Behaviour is unchanged. create_and_buy's mayhem flag defaulted to on, so it takes
--mayhem/--no-mayhem via BooleanOptionalAction rather than a store_true that
would have silently flipped it off.
Two fixes found while threading the values through: the wallet listener read a
global `wallet` from four functions that no longer had one, and find_pool raised
IndexError on a coin that has not graduated instead of saying so.
verify_cookbook_arguments.py enforces the shape — no module-level sys.argv, no
placeholder values, argparse wherever there is input, and every default a usable
value. Confirmed to fail when the old pattern is put back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* fix(docs): replace three dead links and check the rest
The Anchor discriminator citation in anchor_calculate_discriminator.py pointed
at book.anchor-lang.com/anchor_bts/discriminator.html, which Anchor removed when
it restructured its docs. Two links in the README had also moved:
docs.chainstack.com/quickstart/ and docs.chainstack.com/docs/trader-nodes.
book.anchor-lang.com/anchor_bts/discriminator.html
-> www.anchor-lang.com/docs/basics/idl (has a Discriminators section)
docs.chainstack.com/quickstart/
-> docs.chainstack.com/reference/blockchain-apis
docs.chainstack.com/docs/trader-nodes
-> docs.chainstack.com/docs/solana-trader-nodes
All 28 real URLs in the repo now return 200, checked by fetching each one.
verify_documentation_links.py keeps them honest: offline it asserts every URL is
https and that none of the four dead endpoints has come back, and --live fetches
each and fails on 4xx/5xx. It skips the unroutable hosts the offline verifiers
point stub clients at, and example.com, which RFC 2606 reserves for exactly the
placeholder metadata URI a test coin is created with.
The discriminator script also still asked you to edit a constant, which the
argument rule had just outlawed, so it takes the name on the command line and
prints the u64, the byte list and the hex. Cross-checks against the constants in
pumpfun_instructions_v2.py: global:buy_v2 gives [184, 23, 238, 97, 103, 197,
211, 61], which is BUY_V2_DISCRIMINATOR.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* fix(cookbook,tools): detect coins via logs so v1 transactions are not skipped
Solana transaction v1 has been live since 2026-09-15 and solders cannot
deserialize one until 0.29. Three scripts detected coins by deserializing the
transaction envelope, so each went blind to v1:
cookbook/pumpfun/listen/pumpfun_listen_tokens_blocksubscribe.py
30,080 "skipping a v1 transaction" lines in 150s
tools/compare_listeners.py
601 "Failed to process transaction: io error: unexpected end of file"
in 100s, which its own comment read as a transport difference
cookbook/pumpfun/trade/pumpfun_snipe_token_blocksubscribe.py
skipped v1 silently, so a coin created in one was never sniped
All three now route on meta.logMessages, which the RPC has already decoded and
which reads the same for every version. The blocks example keeps the envelope
decode as a genuine fallback for providers that return blocks without logs; the
sniper derives the associated bonding curve from the mint and curve instead of
reading the instruction's account list, verified against
pumpfun_derive_curve_addresses.py on a live coin.
Measured live after the change: the blocks example decoded 88 v0, 6 v1 and 4
legacy creates with zero skips and zero errors, and compare_listeners' block
lane now appears alongside logs and geyser for the same coins.
Upgrading solders was tried first and reverted. 0.29 does decode v1 — 0.26,
0.27.1 and 0.28 all raise "io error: unexpected end of file" on the committed
fixture — but it requires solana>=0.40, which moved TxOpts, MemcmpOpts and
TokenAccountOpts out of solana.rpc.types. 22 files import them and 16 of 19
verifiers failed on the import alone, so that is a migration of its own.
verify_transaction_v1.py gains a check that no cookbook or tools listener
detects by opening the envelope. It found the sniper, which this had missed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
* fix(cookbook): label the transaction version and restore the lost fields
The blocks listener printed "Found CreateEvent in a 0 transaction", which reads
as a count rather than what it is — Solana's transaction version. It now prints
v0, v1 or legacy.
Routing detection through the CreateEvent also dropped two fields the
instruction path used to fill, so every detection showed "Token Standard: N/A"
and no Associated BC. Both come from the event's token_program: the standard is
that program, and the associated bonding curve is an ordinary ATA of the curve
under it. Verified against pumpfun_derive_curve_addresses.py on a live coin —
curve E94izXvHd7dd... derives to He94dArnuprt..., matching what the listener
now prints.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ5HdjTadNUad8i6xis3TR
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(monitoring,core): keep detecting tokens after the Solana transaction v1 cutover
Solana transaction v1 (SIMD-0296/0385) went live at epoch 1035 on 2026-09-15.
`maxSupportedTransactionVersion` turns out to be a whole-frame setting, not a
per-transaction filter: asking blockSubscribe for 0 does not skip the v1
transactions in a block, it nulls out `value.block` for the entire
notification, which reads as a skipped slot. Measured against mainnet
2026-09-16, 60s per run on the pump program: 0 delivered 1 block and 177
nulls, 1 delivered 78 blocks and no nulls.
solders 0.26 cannot deserialize a v1 envelope either, but it does not need to.
The RPC has already decoded it by the time `meta.logMessages` is emitted, and
the platform parsers prefer the CreateEvent in those logs anyway, for the
canonical creator. What broke was the dispatch: _process_block_transactions
decoded the bytes first only to learn which program a transaction touched, and
swallowed the failure in a bare `except Exception: pass`. It now tries
_parse_from_logs on every transaction first and keeps the byte decode as a
fallback, so v1 coins arrive with state_from_event=True and extreme_fast_mode
keeps its zero-RPC contract.
Also raises every other reader to v1, fixes the null-block crash in
listen_blocksubscribe.py, and records that PumpPortal does not push coins
created in a v1 transaction — 0 of 6 measured, against near-complete v0
coverage. That one is upstream and not fixable here.
Closes#197
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98
* fix(examples): survive a v1 envelope and a null block per notification
Raising maxSupportedTransactionVersion to 1 means these examples now receive
the v1 transactions the RPC used to withhold, and solders 0.26 cannot decode
one. manual_buy.py had no handler at all around from_bytes, so the first v1
transaction in a block would have ended listen_for_create_transaction outright
— a regression introduced by the version bump itself.
Guards the null `value.block` in the same three places while here: the key is
present and the value is not for a skipped slot, so the membership test raises
TypeError and costs the whole notification. Same root cause as #196.
Found by CodeRabbit on #212.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98
* fix(examples): raise the shared confirm helper to v1 and decode create_v2's OptionU64
Two gaps the first pass missed, both found by running the changed code against
mainnet instead of only exercising it offline.
tx_status.py confirmed through solana-py's snake_case
`max_supported_transaction_version=0`, a spelling the source scan in
verify_transaction_v1.py did not know about — so the sweep reported clean while
the helper every example confirms through still rejected v1. Verified live: at 0
it raises UnsupportedTransactionVersionMessage on a v1 signature, at 1 it reads
meta.err. The scan now covers both spellings, and setting the threshold above 1
makes it name all seven call sites, so the check is not vacuous.
listen_blocksubscribe.py then turned out to have a third cause behind #197 that
no offline check could reach, because until this branch the example never
received a block to decode: its local create_v2 decoder raises on OptionU64,
the arg added by the 2026-09-15 program upgrade. Live before the fix, 90s: 0
creates decoded, 8 tracebacks. After: 58 creates, 0 tracebacks. Trailing option
args are reported unset rather than as a fabricated default, matching
utils/idl_parser.py and the three wire forms that are live on chain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98
* fix(examples): decode create_v2's trailing optional args in manual_buy
Found by running the script against mainnet with real funds. It crashed before
it could buy:
File "learning-examples/manual_buy.py", line 316, in decode_create_instruction
value = bool(ix_data[offset])
IndexError: index out of range
decode_create_instruction treated every `defined` arg as a 1-byte OptionBool and
read it unconditionally. create_v2's trailing args are positional and legally
absent from the wire, and the 2026-09-15 upgrade added creator_fee_bps as an
8-byte OptionU64, so the fixed read walks off the end on the shorter forms —
which is most coins. Same defect class as the OptionU64 crash in
listen_blocksubscribe.py, and invisible until this branch let either script
receive a block at all.
Absent trailing options are now reported as None, meaning unset, matching
utils/idl_parser.py. Verified live: the script decodes a create with
creator_fee_bps and is_holder_reward both null, then buys 69,387 tokens for
0.003 SOL, confirmed on-chain.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98
* refactor(examples): drop one v1 fixture and trim the other
Two fixtures was one too many. The v0 one was captured to prove the byte-decode
path still works, but it never did: the listener routes on logs first, so the
fixture was caught there and the check passed without exercising what its name
claimed. Feeding it through with the logs stripped shows the byte fallback does
not parse a v0 create_v2 either — the parser's own logs-first branch was always
doing the detection. So the check now reuses the create fixture already
committed for verify_create_v2_optional_args.
The v1 fixture stays, because a v1 transaction cannot be synthesized locally —
solders 0.26 cannot build one — and every verifier here runs offline. Trimmed
from 30KB to 13KB by dropping what nothing reads: innerInstructions, token
balances and pre/post balances. What is left is the envelope, the version and
the logs the listener parses.
Adds the check that was missing: strip the logs and the v1 create must vanish.
That is what makes the log route load-bearing rather than decorative, and it
will start failing the day solders learns to decode a v1 envelope, which is
exactly when the routing could be simplified again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LDiKnRFW49m3T1rKYiz98
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(idl): refresh to upstream 8109141 and decode omitted OptionU64 args
Vendors pump.json, pump_amm.json and pump_fees.json from
pump-fun/pump-public-docs@8109141 (holder rewards coins, cashback
deprecation). create_v2 gains two trailing optionals: creator_fee_bps
(OptionU64) and is_holder_reward (OptionBool).
_is_optional_type only recognised OptionBool, so a create_v2 that omits
creator_fee_bps failed the whole decode and the coin dropped out of the
instruction path — the same failure #184 fixed for is_cashback_enabled.
Reproduced against two live mainnet create_v2 transactions captured
2026-09-15: one omits creator_fee_bps (157-byte instruction data), one
sends it (156 bytes) — both are added as getTransaction fixtures under
learning-examples/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(learning-examples): accept 125-byte bonding curves in dataSize filters
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(learning-examples): replace unrun/recycled curve-size claims with dated live measurements
The 45s/205-update/24-curve figures and the logsSubscribe/same-tx
extend_account claim in the two graduating-token docstrings were
carried over from before the 125/151 filter change and never
re-verified under it. Replaced with fresh 2026-09-15 measurements: a
length-unfiltered programSubscribe run, a direct 125->151 transition
observed on one curve, and a live run of the Geyser script's own
two-group build_subscribe_request confirming both filter groups
actually deliver. Unreproduced claims are now marked UNVERIFIED
in-line instead of stated flatly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(learning-examples): stop enumerating bonding-curve dataSize lengths
extend_account can grow a curve to any length the program allows, so the
CURVE_ACCOUNT_LENS = (125, 151) allowlist in both graduating-token scripts
was whack-a-mole -- a live 256-byte curve was already missed. Drop the
dataSize/datasize server-side filter entirely (discriminator + complete
memcmp already restrict delivery to BondingCurve accounts) and replace the
allowlist with a MIN_CURVE_LEN client-side floor. Collapse the Geyser
script's two named filter groups back to one now that there's no length to
fan out over. Extend verify_curve_account_sizes.py to assert neither script
constructs a dataSize filter and that 125/151/256-byte curves all decode.
Measured live 2026-09-15, filtered vs unfiltered subscribed simultaneously
so both see identical trade activity: ~0.6-0.7% extra traffic on WebSocket,
statistically zero on Geyser -- dropping the filter is effectively free.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(learning-examples): remove untraceable bandwidth figure, session wording
Review of the dataSize-enumeration fix found a docstring figure (90s window,
2/224/866 updates at 256/125/151 bytes) that no command in the fix report
actually produced -- it was a byproduct of the discarded sequential-window
run, never written down with its command+output. Drop it and rely only on
the properly-documented 120s simultaneous measurement already backing the
rest of the paragraph.
Also replace two "carried from an earlier session" UNVERIFIED markers with
the repo's own convention (UNVERIFIED: <what>, no reference to how or when
the file was edited), and give both the update and byte ratio (1.006x /
1.007x) instead of a single derived percentage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(pumpfun): surface is_holder_reward and creator_fee_bps on TokenInfo
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(pumpfun): verify the USDC half of the creator_fee_bps claim
The comment on TokenInfo.creator_fee_bps stated flatly that SOL- and
USDC-paired coins always carry bps 0, but only the SOL half had been
observed live. Found 10 USDC-paired bonding curves (via Dune's indexed
CreateEvent table) and read each one directly through
PumpFunCurveManager.get_pool_state: all 10 show creator_fee_bps=0,
matching every custom-pair coin observed showing nonzero. Dated the
verification in the comment instead of leaving it an unmarked claim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(examples): create holder-reward coins; mark cashback legacy-only
create_v2 now rejects is_cashback_enabled=[true] with 6082
CashbackDeprecated as of the 2026-09-15 upgrade. Add ENABLE_HOLDER_REWARD
to mint_and_buy_v2.py so the trailing create_v2 args can reach
is_holder_reward instead, and relabel the still-live cashback code paths
in core.py, instruction_builder.py and curve_manager.py with the
deprecation date. No cashback account handling is removed anywhere -
existing cashback coins keep trading and accruing exactly as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(pumpfun): resolve quote mints to their real token program
A 2026-09-15 pump.fun upgrade lets create_v2 pair a coin with a Token-2022
quote mint (error 6064 now accepts "SPL Token or Token-2022"), and such
coins already trade on mainnet. quote_token_program() only knew WSOL/USDC
and silently defaulted everything else to SPL Token, deriving associated
token accounts that don't exist on chain for any other quote mint.
Add an async resolver that reads a mint's owner from chain and caches it
for the process, pre-seeded with WSOL/USDC so they stay free. The bot
warms this cache once at startup for every configured quote mint; the
hot path (event parsing, address resolution) only ever reads the cache,
preserving extreme_fast_mode's zero-RPC contract between detection and
submission. quote_token_program() itself is untouched so its
existing call sites keep their non-optional Pubkey return.
Also fixes the standalone pump_v2.py learning-examples helper, which had
the same bug in a form that ignored its argument entirely, and gives
simulate_bot_buy_path.py a quote_amounts entry so it can exercise a
non-SOL-paired coin instead of unconditionally skipping it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(core): name Anchor error codes when reporting a reverted transaction
confirm_transaction only answers whether a transaction landed; meta.err
carries the revert. Logging it raw as {'InstructionError': [idx,
{'Custom': n}]} told an operator nothing, and pump.fun's 2026-09-15
upgrade added 22 new error codes to the pump program and 10 to
pump-amm, so an unnamed number is now more likely to be wrong than
useful. IDLParser.describe_error_code() looks a Custom(N) code up in
an IDL's error table; SolanaClient logs the match alongside the raw
meta.err at both revert sites, so a buy reverting with
BuybackFeeRecipientMissing (issue #175) now prints "6062
BuybackFeeRecipientMissing" instead of a bare 6062.
SolanaClient is shared across pump.fun and letsbonk.fun and has no
record of which program a given transaction invoked, so this only
checks pump.fun's table -- the program the bot's own buy_v2/sell_v2
call directly. A revert on a different program is described against
the wrong table if the code number collides, or left unnamed
otherwise; see the docstring on _describe_program_error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(core): correct get_account_info's return type annotation
It was typed dict[str, Any] but solana-py's AsyncClient.get_account_info
actually returns a solders Account object. Every real caller already
reads attributes (.data, .owner) rather than subscripting, so this was
latent rather than a live bug -- but the wrong annotation is what let
those callers work by accident, and would mislead anyone who trusted
the type and reached for result["owner"] instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(core): tag the revert description with the table it came from
The name appended to a reverted transaction's log line looked like a
fact about whichever program actually reverted, but it is only ever
looked up in pump.fun's error table. This bot also trades letsbonk.fun,
so a letsbonk revert whose numeric code happens to also exist in
pump's table would get a confident, wrong name -- worse than the bare
number it replaced, since a wrong name stops the reader from checking.
Prefixing the description with "pump.fun IDL:" carries the same
limitation the docstring already stated out to the log line itself, so
it's visible without reading the source.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: resync CLAUDE.md and README.md with the 2026-09-15 program upgrade
The bonding curve is 125 bytes now (not 151, no more padding), create_v2
takes eight args instead of six, cashback creation is deprecated (6082
CashbackDeprecated) while existing cashback coins keep trading, quote
assets are no longer just SOL/USDC (QuoteControl, not Global, is the
authoritative mint registry), and holder-reward coins were undocumented
entirely. Add verify_curve_account_sizes.py to both docs' verification
lists, retire the superseded CU comments in instruction_builder.py (the
180k/120k limits are untouched), and fix two stale cashback comments plus
a docstring in client.py that showed a msg suffix code 6062 doesn't have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(trading): refresh quote_token_program_id alongside quote_mint
_refresh_quote_mint synced token_info.quote_mint from freshly-read curve
state but left quote_token_program_id at its construction-time value. A
stale non-None program id wins the `or` in
PumpFunAddressProvider.resolve_quote, so a curve refresh that reveals a
Token-2022 quote mint still resolves the associated_quote_* ATAs under SPL
Token -- an address that does not exist on chain.
cached_quote_token_program is a synchronous dict read, so this keeps
extreme_fast_mode's zero-RPC-call contract between detection and
submission.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(examples): resolve the quote mint's real token program in manual buy/sell
manual_buy.py and manual_sell.py derived the quote ATA's owning program
through pump_v2.quote_token_program, which defaults any mint outside the
WSOL/USDC map to SPL Token. A pump.fun upgrade verified 2026-09-15 lets
create_v2 pair a coin with a Token-2022 quote mint (error 6064 now accepts
"SPL Token or Token-2022"), so that default derives an ATA that does not
exist on a Token-2022-quoted coin -- and these scripts submit real
transactions, so the failure burns one.
Both now call pump_v2.resolve_quote_token_program (free for WSOL/USDC via
its cache, one RPC call otherwise) and thread the resolved program into
both the buy_v2/sell_v2 instruction and the quote ATA creation.
Verified against a real Token-2022-quoted pump.fun coin found via Dune
(pumpdotfun_solana.pump_evt_createevent, mint G5Ve125Poi7KoDRP5uLsgu5NQdm3KoqfyUkq4cqtTo4r,
quote_mint Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh, confirmed Token-2022-owned
by RPC read): the old default derives associated_quote_bonding_curve
FViEZvbcYjoSwGE2b37HiCXmPT3N7kSd1nQavFzcD7Yb, which does not exist on chain;
the fixed resolution derives DfxcBKXTLDqobNEmR5JkaBeoKqNbJE1vKbtQVbowXz4x,
which exists on chain and is owned by the Token-2022 program.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(core): correct get_multiple_accounts's return type annotation
Declared as list[Any] while it actually returns a list of solders Account
objects (or None for missing accounts) -- the same stale-annotation
pattern already corrected on get_account_info.
Checked the one caller, PumpFunCurveManager.get_pool_state_and_token_program,
which already unpacks the two entries and reads .data / .owner off them
directly rather than subscripting -- no live bug behind the wrong
annotation here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(examples): use the public decode API in the optional-args verifier
check_omitted_option_u64_decodes and check_option_u64_present_decodes
reached into IDLParser._decode_instruction_args behind # noqa: SLF001
instead of the public decode_instruction(data, keys, accounts) already
used by every other check in this file. The fixtures already carry valid
accounts/keys (previously discarded as data, _, _), so the public method
expresses the same check -- confirmed it returns byte-for-byte identical
decoded args as the private call did.
Confirmed the check stays load-bearing: reverting _is_optional_type to a
narrow == "OptionBool" comparison drops the verifier to 2/7 checks passed,
exit 1, including the two checks touched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: correct unverified claims from the final branch review
Text-only fixes from the whole-branch review — no code defects found.
- CLAUDE.md: create_v2 trailing-arg claim was circular (sourced from our own
minter script) and wrong — the three committed fixtures show 0, 1 and 2
trailing args, never 3. Rewritten with the counted byte lengths and dated;
the unobserved 3-arg form is now marked UNVERIFIED instead of asserted.
- src/interfaces/core.py, CLAUDE.md: corrected an inflated "25 SOL-paired"
sample to the actual 21 (25 coins captured live, 4 of them custom-pair).
- instruction_builder.py: buy_v2/sell_v2 CU comments cited figures from a
task brief that were never re-measured on this branch. Re-measured via
simulate_v2_trades.py on 2026-09-15 (106,677 / ~77,196 CU) and updated the
comments; the configured 180k/120k limits are unchanged.
- simulate_bot_buy_path.py: removed a working-session reference ("this
task") from a comment.
- get_graduating_tokens.py, get_graduating_tokens_geyser.py's sibling docs
(pump_v2.py, fetch_price.py, get_bonding_curve_status.py,
verify_pumpportal_buy_path.py): made the curve-length story consistent
(125/151/256, don't enumerate lengths) instead of a stale "125 or 151"
framing; resolved an UNVERIFIED marker that mint_and_buy_v2.py already
answers (extend_account can share a transaction with create_v2); removed
a redundant restated clause in the bandwidth-measurement paragraph.
Verified: all 7 verify_*/simulate offline+mainnet checks still exit 0,
buy_v2/sell_v2 account counts unchanged at 27/26.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(trading): resolve quote-mint decimals instead of defaulting to 9
quote_decimals()/quote_units_per_token() defaulted every quote mint outside
the WSOL/USDC map to SOL's 9 decimals, mis-scaling max_sol_cost/min_sol_output
by up to 1000x for a custom Token-2022 quote mint (verified: 6 decimals).
Extend resolve_quote_token_program's existing mint-account fetch to also
parse and cache decimals from the same read (byte offset 44, shared by SPL
Token and Token-2022), so the fix costs zero extra RPC calls and the
zero-RPC extreme_fast_mode contract holds. Mirrored the same treatment in
learning-examples/pump_v2.py's standalone copy.
Also decode is_holder_reward in the instruction-only create_v2 parse path
(a landed transaction proves it, since the program rejects the flag when
globally disabled) while leaving creator_fee_bps at its default, since
upstream docs say it's ignored for SOL/USDC-paired coins and nothing reads
it today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(examples): strengthen optional-arg verifier assertions
Fix verify_create_v2_optional_args.py to properly assert value equality
rather than just presence when checking optional arguments:
- check_omitted_option_u64_decodes: verify both creator_fee_bps and
is_holder_reward are exactly None (not just present in dict)
- check_option_u64_present_decodes: decode the fixture, verify
creator_fee_bps shape is {"field_0": <value>}, and assert the actual
value (0) rather than just non-null
- Fixture decodes to creator_fee_bps={'field_0': 0}, is_holder_reward=None
Also fix README.md line 166: "Three examples" → "Four examples" to match
the four verification commands listed.
Temporary revert of idl_parser.py's _is_optional_type confirms the
strengthened assertions are load-bearing: changing from startswith("Option")
back to == "OptionBool" causes 4 of 7 checks to fail.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkvFUgSiZoaF7w4xugrgBc
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extreme-fast mode submits with no RPC calls between detection and buy
when the listener parsed the on-chain CreateEvent (geyser/logs/blocks):
TokenInfo.state_from_event carries the canonical creator, mayhem/cashback
flags and quote_mint, so the pre-buy curve refresh is skipped. The geyser
listener now delegates to the event-first parser instead of inlining
instruction decoding, which also catches create_v2 transactions that omit
the trailing is_cashback_enabled byte.
Listeners without event data (pumpportal) keep the refresh, hardened per
issue #170: bonding curve derived from the mint instead of the payload's
bondingCurveKey, curve and mint read in one slot-consistent
getMultipleAccounts (correcting the guessed token program), and an
unreadable curve now skips the token within trade.curve_refresh_budget
instead of submitting a buy built from guessed accounts.
Machine-checked by verify_extreme_fast_zero_rpc.py and
verify_pumpportal_buy_path.py; validated live via simulate_bot_buy_path
on both geyser and pumpportal paths.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(learning-examples): replace getProgramAccounts scan in get_graduating_tokens
The pump.fun program owns over 10M accounts and no provider will scan it, so
get_graduating_tokens.py could not run at all (#178). getProgramAccountsV2 is
not a fix: it is a provider extension rather than core Agave, and its `limit`
is a scan budget, not a result count, so one filtered answer costs ~1000
sequential pages.
Rewrite discovery onto filtered programSubscribe, which applies dataSize and
memcmp server-side and is accepted even by public api.mainnet-beta.solana.com.
Every write to a curve pushes the full 151-byte account, so progress is
computed per update with no accumulated state. Add a Geyser sibling that
reports the same thing with the slot and signature behind each update.
Also fix two bugs that would have survived the rewrite: the mint lookup
queried SPL Token, which returns nothing for the Token-2022 ATAs that every
create_v2 coin uses, and the threshold was a hardcoded constant rather than
Global.initial_real_token_reserves.
Closes#178
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(learning-examples): drop the graduation cutoff that filtered nothing
zero_prefix_gate offered a cutoff so high that no coin could fail it, so for any
--min-progress below 64.5% the subscription was unfiltered while the banner
reported a filter as active. Offer only the three cutoffs that actually narrow,
and say plainly when none applies.
Rewrite the threshold notes in both scripts in plain English: which cutoffs
exist, whether a given threshold gets one, and the part that matters — the
pre-filter saves bandwidth but does not decide the answer, so the requested
percentage is honoured either way. The banner now names the cutoff as a
percentage instead of byte offsets.
Both directions were checked against mainnet by running the filtered and
unfiltered subscriptions side by side for a minute, on both transports: the
filtered stream matched the below-cutoff set exactly, with 143 of 168 curves
above the cutoff on WebSocket and 128 of 154 on Geyser.
Also document the two scripts in the README example table, and record under
throughput that getProgramAccounts over the whole pump.fun program is no longer
served by any provider.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audited every script in learning-examples/ against mainnet.
Broken — found by running them, all invisible offline:
- listen_geyser.py crashed with IndexError after ~11 coins: it never resolved
v0 address-lookup-table accounts, which geyser reports in
meta.loaded_writable_addresses / loaded_readonly_addresses. Resolving them
removes the crash and brings detections level with the WebSocket listeners,
35 coins each per 150 s.
- compare_listeners.py logged 13,090,862 error lines / 888 MB in 150 s and never
printed its own 30-second report: the inner recv() loop caught ConnectionClosed
in a broad handler that only logged, so every following recv() raised at once
and the outer reconnect handler was unreachable. Now 12 KB and exit 0. Same
shape fixed in compare_migration_listeners.py, listen_blocksubscribe.py and
extract_blocksubscribe_transactions.py; the last two also gained the reconnect
loop their siblings already had.
- decode_from_gettransaction.py matched instructions on account count instead of
discriminator, reporting a real 19-account create_v2 as claim_cashback with
every account under the wrong name. It also walked only top-level
instructions, and in 40 consecutive pump.fun transactions there was 1
top-level pump instruction against 8 inner ones.
- decode_from_blocksubscribe.py crashed on every real create_v2: on chain the
trailing args are variable length, 0001 in one tx and 00 in another, so
is_cashback_enabled can be absent entirely.
- poll_bonding_curve_progress.py polled a hardcoded dead mint and took no argv.
Obsolete:
- Delete listen_blocksubscribe_old_raydium.py. Seven minutes on mainnet produced
0 initialize2 events while the wrapper listener caught 3 real migrations.
- Delete the duplicate geyser stubs and protos under listen-new-tokens/. The
protos were byte-identical to src/geyser/proto and the stubs had drifted; both
geyser examples now import src.geyser.generated.
- Recapture all four fixtures. The old ones were from Aug 2024 and included a
49-byte pre-creator bonding curve.
Behind the protocol:
- fetch_price.py, get_bonding_curve_status.py, poll_bonding_curve_progress.py
and decode_from_getaccountinfo.py never read quote_mint and scaled by a
hardcoded 1e9. Against a live USDC-paired curve the price was off by 1000x.
- get_pumpswap_pools.py stopped parsing at coin_creator and missed the i128
virtual_quote_reserves. Live pools carry 17.5845 SOL of them, which
under-prices by 3.5-23.9% when ignored.
Duplication and naming:
- Merge manual_buy_cu_optimized.py into manual_buy.py --cu-optimized. The
deleted file's docstring said 512 KB while its code used 16 MB; simulation
confirms 512 KB and 4 MB both fail MaxLoadedAccountsDataSizeExceeded on
Token-2022 mints, so 16 MB is the correct value.
- Merge listen_logsubscribe_abc.py into listen_logsubscribe.py. Its ATA
derivation hardcoded the legacy token program, so every Associated BC it
printed for a Token2022 coin was an address that does not exist on chain.
Fixed on merge and cross-checked 59/59 against on-chain accounts.
- Remove 19 dead symbols. BREAKING_FEE_RECIPIENTS is still live in the PumpSwap
scripts and stays there.
- Normalize naming: kebab-case directories, RPC method names as one lowercase
token, scripts verb-first. Rules documented in CLAUDE.md.
get_graduating_tokens.py is knowingly left broken: getProgramAccounts over the
whole pump program is now rejected by providers and it needs a
getProgramAccountsV2 rewrite, which belongs in its own PR.
Verified: both offline gates pass, all 41 examples parse, every read-only script
exercised on mainnet against SOL- and USDC-paired coins, no new ruff findings
(427 -> 413). No script that spends real funds was run.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move the "Also by Chainstack" callout back to its original position
just below the walkthrough link, in its original blockquote style, so
it stays visible without scrolling. The previous README rewrite had
relocated it to the bottom of the file.
Note that the linked walkthrough doc lags behind the code, and point
readers at the README for setup and configuration instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrite README.md around setup and configuration: fix the clone URL,
document the actual .env variable names, add tables for bots/*.yaml and
the learning-examples directories, and drop the empty changelog, the
2025 roadmap, and the protocol deep-dives that duplicated CLAUDE.md.
Make CLAUDE.md the single agent guide and symlink AGENTS.md to it.
AGENTS.md carried wrong env var names, a stale Python floor, and a
config key that does not exist; its safety rules move into CLAUDE.md.
Document that `uv pip install -e .` puts src/ on sys.path, so imports
are `from utils.logger import ...` rather than `from src.utils...`.
Delete .cursor/rules/, .kiro/steering/, and .windsurf/rules/ - three
byte-identical copies of rules referencing APIs that do not exist in
src/. All three tools read AGENTS.md natively.
Fix pyproject.toml:
- requires-python >=3.9 -> >=3.11; the code uses `X | None` (3.10+) and
ruff already targets py311
- drop borsh-construct and construct-typing, neither of which is
imported anywhere (construct-typing still resolves via solana)
- move grpcio-tools to the dev group; it is protoc, needed only to
regenerate the geyser_pb2 stubs, never at runtime
- move dev deps from [project.optional-dependencies] to
[dependency-groups] so `uv sync` installs ruff, making the documented
`ruff check` / `ruff format` commands actually available
Also gitignore .claude/settings.local.json, which is per-developer.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refresh the vendored IDLs from pump-fun/pump-public-docs @ 9c82f61 and move all
pump.fun trading onto the v2 instruction interface. This is required, not
optional: legacy buy/sell cannot trade coins paired against a quote asset other
than SOL, and USDC is already whitelisted in the on-chain Global account.
Protocol changes absorbed:
- buy_v2 (27 accounts) / sell_v2 (26 accounts) replace the legacy instructions.
Every account is mandatory and the order is identical for all coins, so the
conditional cashback/mayhem account lists are gone. Legacy remains available
via PumpFunInstructionBuilder(use_legacy_instructions=True).
- BondingCurve is 151 bytes: virtual_sol_reserves -> virtual_quote_reserves,
real_sol_reserves -> real_quote_reserves, plus quote_mint at offset 83. Old
field names are kept as aliases so existing callers keep working.
- v2 instruction data drops the track_volume OptionBool; amounts are in the
quote mint's raw units rather than always lamports.
- create_v2 carries a non-SOL quote mint as optional remaining accounts 17-19,
and CreateEvent gained quote_mint, so extreme_fast_mode can resolve the quote
asset without an extra fetch.
USDC support: new trade.quote_amounts and filters.allowed_quote_mints config,
accepting "sol"/"usdc" aliases or raw mints. Amounts are per-quote-mint because
1 USDC and 1 SOL are not interchangeable. A coin whose quote mint has no
configured amount is skipped rather than traded at the wrong size, so SOL-only
configs are unaffected.
Bug fixes found while verifying:
- The logs and blocks listeners set no websocket max_size, so any frame over
1 MiB closed the connection with 1009 and the token in it was lost. Raised
to 32 MiB.
- PumpSwap priced against the raw quote vault balance, ignoring the new
Pool.virtual_quote_reserves (i128 at offset 245; live pools are 301 bytes).
Upstream's note that this field is 0 everywhere is out of date: a live pool
carries 17.58 SOL against a 148 SOL vault, a 10.15% price error.
- The seller read curve state once at confirmed commitment and silently fell
back to create-time values, risking a stale creator_vault and ConstraintSeeds.
It now retries at processed, matching the buyer.
- Account cleanup would burn wrapped SOL when force_burn was set, destroying
value that closing the account returns. WSOL is now closed without burning.
- The mint scripts treated a landed transaction as a successful one, so a
reverted buy printed as success. They now assert the on-chain result.
Compute unit limits retuned from mainnet measurements: buy 100k -> 180k,
sell 60k -> 120k. Mint-and-buy is no longer atomic, because create_v2 plus
buy_v2 exceeds the 1232-byte transaction limit; both mint scripts send two
transactions.
Adds learning-examples/pump_v2.py as one shared, standalone v2 toolkit for the
example scripts, and three verification scripts: an offline layout check
against the IDL, a no-funds mainnet simulation, and a live listener matrix that
buys, sells and closes the ATA per listener.
Verified on mainnet: all four listeners (geyser, logs, blocks, pumpportal) and
all eight example scripts completed a real buy, sell and ATA close, each
confirmed by reading the transaction result back rather than trusting
confirmation alone. The USDC path is verified structurally only; no USDC-paired
coin could be found on-chain to exercise it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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>
Replace idl/pump_fun_idl.json and idl/pump_fees.json with the latest from
pump-fun/pump-public-docs @ 7de0b95 (2026-04-23). pump_swap_idl.json was
already up to date.
Drops the idl/upstream/ vendor copy added in 7864504 in favor of
overwriting in place — the diff lives in git history. README updated to
point at pump-public-docs as the source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot of pump-fun/pump-public-docs @ 7de0b95 (2026-04-23) under
idl/upstream/ — pump.json, pump_amm.json, pump_fees.json and their .ts
counterparts. Used for local diffing against the IDLs the bot loads from
idl/; not imported at runtime. README links to the new directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): add RPC rate limiting, retry logic, and 429 handling
Addresses #44 — users on free-tier RPC endpoints hit HTTP 429 errors
during buy transactions due to no rate limiting or retry handling.
- Add TokenBucketRateLimiter (new file: src/core/rpc_rate_limiter.py)
- Gate all RPC methods through rate limiter (both post_rpc and solana-py calls)
- Rewrite post_rpc() with retry loop, exponential backoff, jitter, and
specific 429 detection with Retry-After header support
- Replace per-call aiohttp session with shared persistent session
- Wire node.max_rps from YAML bot config through to SolanaClient
- Fix cleanup manager and learning example to use SolanaClient abstraction
instead of bypassing it via get_client()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(core): address CodeRabbitAI review feedback on rate limiting PR
Validate max_rps > 0 in TokenBucketRateLimiter to prevent ZeroDivisionError
and infinite loops with fractional values. Add asyncio.Lock to _get_session
to fix race condition, handle non-numeric Retry-After headers gracefully,
replace dead json.JSONDecodeError with aiohttp.ContentTypeError, and combine
burn+close into a single transaction in cleanup example.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: document RPC rate limiting feature in README
- Add section on built-in RPC rate limiting with token bucket algorithm
- Document configurable max RPS and automatic retry logic
- Update roadmap to mark "Configurable RPS" as completed
- Clarify benefits of rate limiting for provider compliance
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: use math.ceil for burst_size to handle fractional max_rps
- Replace int(max_rps) with math.ceil(max_rps) in burst_size calculation
- Prevents infinite loop when max_rps < 1.0 (e.g., 0.5 RPS would result in burst_size=0)
- Ensures burst_size is always at least 1 for valid fractional rates
- Addresses CodeRabbit feedback on rpc_rate_limiter.py:27
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(core): validate burst_size and fix table pipe consistency
Address remaining CodeRabbitAI review feedback: add burst_size
validation guard, fix TRY003 lint (use msg variable for ValueError),
break long line under 88 chars, and fix MD055 table pipe style in README.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(core): separate 429 retry budget from error retries in post_rpc
429 responses no longer count against max_retries — they use a dedicated
max_429_retries counter (default 10) so free-tier users hitting rate
limits won't exhaust retries prematurely. Also refresh the aiohttp
session inside the retry loop to avoid stale references after network
failures, and fix cleanup log message accuracy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Anton Sauchyk <antonsauchyk@gmail.com>