Commit Graph
296 Commits
Author SHA1 Message Date
ebbfa9f124 chore(deps): upgrade to solders 0.29 / solana 0.40 and bring the rest of the dependencies current (#217)
* chore(deps): upgrade solders to 0.29 and solana to 0.40 in cookbook and tools

solana 0.40 moves TxOpts (now TxOptsModel), MemcmpOpts and TokenAccountOpts
from solana.rpc.types to solana.rpc.core, and spl.token's *Params dataclasses
to spl.token.models as pydantic models — validated and keyword-only. Three
call sites only failed against mainnet: MemcmpOpts.bytes now takes a base58
str, SyncNativeParams was built positionally, and confirm_transaction was
passed a bare "confirmed".

Also moves every cookbook and tools send path off the legacy Transaction onto
VersionedTransaction + MessageV0, and teaches the getTransaction decoder the
json and base64 encodings alongside jsonParsed — the base64 v1 fixture needs
solders 0.29 to deserialize at all.

src/ is untouched and still on the old imports; the offline suite is 3/19
until that lands.

Verified on mainnet: a 0.003 SOL buy_v2/sell_v2 round trip confirmed and the
wallet returned to SOL only, all six listeners and every read-only script run
live, five trade paths simulated with error=None.

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

* chore(deps): move the bot onto solders 0.29 and solana 0.40

Same three API moves as the cookbook commit, now in src: TxOpts is
TxOptsModel in solana.rpc.core, and spl.token's BurnParams/CloseAccountParams
come from spl.token.models. build_and_send_transaction builds a
VersionedTransaction from MessageV0 with no lookup tables, so the bot no
longer sends a legacy transaction anywhere.

verify_transaction_v1 proved the log route by stripping the logs and expecting
nothing back, which only held while solders could not read a v1 envelope. On
0.29 it can, so the check is inverted: make the envelope unreadable, keep the
logs, and the create must still arrive. A second check pins the envelope
fallback, so a decode regression is reported rather than silently absorbed by
the log route.

Offline: 19/19. Live on mainnet, funds returned:
- tools/simulate_bot_buy_path: zero-RPC buy path, err=None, 107,015 CU
- tools/simulate_v2_trades: buy_v2 95,573 CU, buy+sell 174,190 — both inside
  the existing 180k/120k limits, so no retune
- tools/live_v2_round_trip: buy_v2 + sell_v2 both landed
- tools/live_listener_matrix: all four listeners detected; logs and pumpportal
  ran full round trips; geyser and blocks both drew a coin on an unconfigured
  quote mint and correctly refused to size a buy. Every ATA closed.

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

* chore(deps): bring the rest of the dependencies to current

Every remaining dependency is now at its latest release, with the floors
raised to the versions actually tested rather than the oldest that resolves:

  grpcio        1.73.1 -> 1.84.0    grpcio-tools 1.73.1 -> 1.84.0
  protobuf      6.33.5 -> 7.36.2    uvloop       0.21.0 -> 0.22.1
  pyyaml        6.0.2  -> 6.0.3     python-dotenv 1.2.2 -> 1.2.3
  construct     2.10.70 (floor)     websockets   17.1 (floor)
  ruff          0.12.4 -> 0.16.8

aiohttp and base58 were already current. winloop is left at its old floor:
it is Windows-only and cannot be installed, let alone tested, here.

protobuf 7 arrives with grpcio-tools 1.84 and is the only major. The
committed geyser stubs are gencode 6.31.1 and still pass
ValidateProtobufRuntimeVersion against it; they are not regenerated, because
the committed files import each other absolutely and protoc does not emit
that, so the output would need rewriting. Noted in CLAUDE.md with the
verification date.

ruff 0.16 needs no config change. The repo-wide baseline moves 1680 -> 1713
because the rules moved, not the code; CLAUDE.md carries the new figure so it
is not misread as a regression.

Verified per dependency, not just in aggregate: all four bot configs load
under the new pyyaml/dotenv; the logs, blocks and pumpportal listeners run
under the uvloop policy bot_runner installs; both geyser paths (token
listener and the account-filtered graduation watcher) run under grpcio 1.84 /
protobuf 7. Offline 19/19, including verify_listener_cancellation for
websockets. One more live buy_v2/sell_v2 round trip on the final set, both
landed, ATA closed.

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

* fix(utils): redact RPC credentials from every log record

The provider endpoints carry their API key in the URL and the HTTP clients log
a full request URL at INFO, so anything that raises the root logger to INFO
prints the key. tools/cleanup_accounts.py did, four times, in a terminal.

It was already silencing httpx and httpcore by name. That stopped working the
moment solana-py 0.40 switched to httpx2: the rename took the guard with it
and nothing said so. Naming the clients is now a convenience on top of the
real control — install_secret_redaction masks the value itself, at log-record
creation, whichever logger emits it.

It redacts the *rendered* message rather than record.msg plus the string
arguments. The first version of this did the latter and still leaked: httpx2
passes its URL as a URL object, so the type check skipped the only argument
carrying the key. Confirmed by forcing httpx2 back to INFO and watching the
key print anyway. The host is kept, so a log line still says which provider
answered.

Installed from get_logger, from both logging.basicConfig sites, and from
setup_file_logging before it attaches the handler — logs/ outlives a terminal.

verify_no_rpc_credentials_logged covers all of it, including the non-str
argument. Checked against the defeated version first: it fails there and
passes here. Suite is 20/20.

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

* docs(agents): make a real bot run the verification, not the simulations

The ground rule said never to run a bot with real funds, which points at the
tools/ harnesses instead. Those call the buyer, seller and listeners directly:
they skip bot_runner, config loading, the trader loop, the exit strategy and
cleanup, so passing there says nothing about the paths only a full run reaches.
Verification is a real run, with explicit approval for that session, covering
every mode the change touches and ending with the wallet holding SOL only.

Also records why a run can look clean when nothing happened: pump_bot runs every
bots/*.yaml, three of the four committed configs ship enabled: false, and with
all of them disabled it prints nothing and exits 0. The log file is the tell.

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

* docs(utils): keep the redaction comment provider-neutral

The parameter list is what matters, not which provider uses which spelling.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 13:28:43 +02:00
41b971147a refactor(cookbook): restructure learning-examples, add missing actions, fix quote decimals (#216)
* 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>
2026-09-23 09:34:40 +02:00
Anton SauchykandClaude Opus 5 f4dca24cdd docs(agents): compact the verification sections and refresh stale facts
Six per-issue "Verifying ..." sections (245 lines, 40% of the file) restated
rationale that already lives in each verifier's docstring, at similar length.
Replace them with one table covering all 18 scripts and an Invariants section
keeping only the rules that constrain new code rather than a bug already fixed.

Also fixes what had gone stale: the four verifiers from #206/#211 were never
listed, config_loader.py sits at the package root and not under core/, and the
ruff baseline is ~1660 rather than ~1700.

618 -> 477 lines. Protocol notes and listener pitfalls are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 15:46:56 +02:00
13b9ec2baf fix(monitoring,core): keep detecting tokens after the Solana transaction v1 cutover (#212)
* 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>
2026-09-17 08:27:30 +02:00
Anton SauchykandGitHub e1c09f6605 fix(trading,core): make exit sells safe to retry, bound RPC wall time, detect bonk tokens (#211) 2026-09-16 18:42:29 +02:00
Anton SauchykandGitHub a53175f1eb fix(trading,monitoring): keep landed buys, retry reverted sells, stop dropping blocks (#206) 2026-09-16 17:01:36 +02:00
Anton SauchykandClaude Opus 5 5ac482256b docs: record that quote-mint decimals are resolved from chain
The quote-asset notes covered resolving a quote mint's token program but not
its decimals, which is the more dangerous of the two: amounts like
max_sol_cost are in the mint's raw units, so assuming 9 decimals for a
6-decimal mint overstates the cap by 1000x and effectively disables slippage
protection. Both facts come from the same getAccountInfo, so the read costs
nothing extra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 09:41:40 +02:00
55ae2afd82 feat(pumpfun): resync with the 2026-09-15 program upgrade (#202)
* 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>
2026-09-16 09:36:44 +02:00
a0540fdc9e fix(trading): price tp/sl exits off the trigger price and retry a reverted sell (#193)
* 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>
2026-08-24 10:22:47 +02:00
RecoordinateandGitHub b367191267 fix(ci): restore the undefined warningMessage in spam-detection workflow (#187)
The github-script step references warningMessage in createComment, but its
definition was removed in 9ac0c27 while the usage was kept. When a comment
tripped the threshold, the step threw 'ReferenceError: warningMessage is not
defined', which the surrounding try/catch swallowed — so the scam-warning
comment was never posted (only the label was applied). Restore the message,
built from the variables already in scope (author, foundPatterns,
suspiciousLinks).
2026-08-22 12:46:33 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9ab74b91e0 build(deps): bump aiohttp in the uv group across 1 directory (#188)
Bumps the uv group with 1 update in the / directory: [aiohttp](https://github.com/aio-libs/aiohttp).


Updates `aiohttp` from 3.14.1 to 3.14.3
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.14.1...v3.14.3)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.3
  dependency-type: direct:production
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-22 12:46:07 +02:00
9a3f2c0bfb fix(utils): decode omitted trailing optional args in IDL instruction decoder (#185)
create_v2 instructions can legally omit the trailing is_cashback_enabled
OptionBool from the wire; decode_instruction rejected that form, so every
consumer of parse_token_creation_from_instruction silently dropped those
coins (the committed blocksubscribe fixture carries one such 145-byte
create_v2).

- When the arg buffer is exhausted and every remaining arg is
  option-typed (native option or pump.fun's OptionBool), report them as
  unset (None) instead of failing the decode; mandatory args still fail.
- Count trailing optional args as 0 bytes in the instruction min-size
  table.
- Decode Anchor's native option type (1-byte tag + value), previously
  unsupported entirely - update_buyback_config's Option<u64> could not
  even be sized.
- Add learning-examples/verify_create_v2_optional_args.py machine-checking
  the omitted, present, truncated-mandatory and native-option forms plus
  the event-parser path against the committed fixture.
- Drop the append-0x00 workaround in verify_extreme_fast_zero_rpc.py and
  update the stale CLAUDE.md note about the strict decoder.

Fixes #184

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 20:32:15 +02:00
Anton SauchykandClaude Fable 5 c16fc856d0 fix(deps): bump setuptools to 83.0.0 for CVE-2026-59890
Resolves Dependabot alert #33 (GHSA-h35f-9h28-mq5c): MANIFEST.in
exclusion bypass in sdist builds via NFC/NFD Unicode normalization
collision on macOS. setuptools is a transitive dependency of
grpcio-tools (dev group); lockfile-only change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 20:19:15 +02:00
Anton SauchykandClaude Fable 5 04442302f4 docs: sync extreme-fast refresh description with zero-RPC behavior
CLAUDE.md's coin-creation note and simulate_bot_buy_path's docstring still
described the pre-#183 unconditional curve refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:39:54 +02:00
7bc6f25508 feat(trading): zero-RPC extreme-fast buys and listener race hardening (#170) (#183)
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>
2026-07-29 18:29:03 +02:00
Anton SauchykandClaude Opus 5 bc59a2257d ci(auto-assign): assign new issues to smypmsa
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:01:28 +02:00
678fa19fe4 fix(learning-examples): replace getProgramAccounts scan in get_graduating_tokens (#182)
* 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>
2026-07-29 16:52:04 +02:00
Anton SauchykandClaude Opus 5 dcf66ec1b3 docs(agents): correct stale layer chain, ruff baseline, and fund-spending script rule
Verified every claim in CLAUDE.md against the code after the recent debugging,
IDL, bot, and learning-example updates. Four were wrong:

- The dependency chain listed `utils` before `interfaces`, but
  utils/idl_manager.py imports interfaces.core and interfaces imports nothing
  internal. It also made trading and monitoring peers when trading imports
  monitoring, and omitted cleanup and geyser entirely.
- Ruff baseline said ~2400 errors; `uv run ruff check` reports 1676.
- `live_*` was presented as the marker for scripts that spend real funds. Only
  2 of the 13 scripts that submit transactions carry that prefix — manual_*,
  mint_and_buy* and cleanup_accounts.py spend too.
- compare_listeners.py was cited without its path, ambiguous against
  listen-migrations/compare_migration_listeners.py.

The protocol section (v2 account counts, BondingCurve offsets, PumpSwap i128,
PDA seeds, fee-recipient sets, tx-status rules) checks out against
idl/pump_fun_idl.json and src/; both offline verifiers still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 15:38:47 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
aa3ac28943 build(deps): bump the uv group across 1 directory with 2 updates (#180)
---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-version: 1.2.2
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: aiohttp
  dependency-version: 3.14.1
  dependency-type: direct:production
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 15:26:42 +02:00
a33f4fd035 fix(learning-examples): repair crashing listeners and decoders, prune obsolete paths, unify naming (#181)
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>
2026-07-29 15:26:26 +02:00
Anton SauchykandClaude Opus 5 5c1fb7eb28 docs: fix README callout placement, flag walkthrough as lagging
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>
2026-07-29 14:09:47 +02:00
7727015548 docs: consolidate agent docs, rewrite README, prune deps (#179)
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>
2026-07-29 14:07:17 +02:00
836d873d27 fix: report on-chain trade outcomes correctly, and repair every broken learning example (#177)
* fix(tx-status): read meta.err before reporting a trade succeeded

confirm_transaction answers "did this signature land in a block?", never
"did it succeed". A landed transaction can have reverted, and RPC reports
that only in meta.err. Reading whether a call threw instead of reading
meta.err produced false results in both directions.

False success — a reverted trade reported as confirmed:

- ten examples (pump.fun, pumpswap, letsbonk) printed "Transaction
  confirmed" without checking meta.err
- src/cleanup/manager.py discarded confirm_transaction's boolean and
  logged "Closed successfully" unconditionally, so a reverted close
  reported rent as reclaimed while the account stayed open
- learning-examples/cleanup_accounts.py did the same

False failure — a good trade reported as unconfirmed:

- _get_transaction_result omitted maxSupportedTransactionVersion, so the
  RPC answered -32015 for every versioned (v0) transaction. meta.err was
  unreadable and a successful trade read back as failed. The bot sends
  legacy transactions, which is the only reason this was survivable.
- confirm_transaction raised TypeError on a base58 str (solana-py wants a
  Signature) while _get_transaction_result raised on a Signature (not
  JSON serializable). Both were swallowed by a broad except into "not
  confirmed". The annotations pointed the wrong way too:
  build_and_send_transaction returns Signature, not str.

Changes:

- add learning-examples/tx_status.py — assert_transaction_succeeded and
  confirm_and_assert, replacing the copy duplicated in mint_and_buy{,_v2}
- wire it into the ten examples that confirmed without checking
- read the boolean in both cleanup paths
- normalize str/Signature at the client boundary; correct the annotations
- send maxSupportedTransactionVersion: 0 on getTransaction
- split verify_transaction_succeeded out of confirm_transaction so the
  meta.err check can run against a transaction that landed earlier

Two reporting bugs found while testing the above:

- live_v2_round_trip read balances at solana-py's default (finalized)
  commitment while confirming trades at "confirmed", so the end read saw
  pre-trade state and it printed "net change: +0.000000000 SOL" after a
  real round trip. Verified: on a busy account finalized trails confirmed
  by ~263k lamports.
- cleanup_accounts produced no output at all, success or failure, because
  get_logger attaches no handler and only the bot installs one. httpx is
  pinned to WARNING alongside it — the RPC endpoint carries an API key.

Adds learning-examples/verify_tx_status_checks.py: offline stub checks, a
scan that fails if an example confirms without checking meta.err, an AST
check that nothing in src/ discards the boolean, a guard that
getTransaction opts into v0, and --live, which replays issue #175's three
signatures against mainnet and requires both layers to reject them on
meta.err (Custom: 6062) rather than on a failed fetch. The two src/ guards
were mutation-tested: reintroducing each bug makes them fail.

The BuybackFeeRecipientMissing (6062) half of #175 was already fixed by
the buy_v2/sell_v2 migration in 02343b7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFrxcZfCf9C76voCb7M8Pa

* fix(client): retry RPC timeouts instead of letting them escape post_rpc

aiohttp signals a request timeout with asyncio.TimeoutError, which is not
an aiohttp.ClientError. post_rpc caught only the latter, so every RPC
timeout propagated to the caller unretried — and str() on it is empty, so
whatever logged it printed a blank reason.

Found while running learning-examples/live_listener_matrix.py: three of
the four listeners died mid-run with "CRASHED: " and no message. The
endpoint was answering getHealth in ~100ms while getAccountInfo hung past
60s, and every caller that touches it (sol_balance, ata_is_closed,
AccountCleanupManager.cleanup_ata) went down with it. With the retry in
place the same run degrades to a logged failure and completes.

Also makes the verifier report a raising check as a failure rather than
aborting the run — several checks assert that a call does NOT raise, so
the raise is the finding and the remaining checks still need to report.

Covered by "RPC timeouts are retried, not raised" in
verify_tx_status_checks.py, mutation-tested against the one-exception
version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFrxcZfCf9C76voCb7M8Pa

* fix(examples): raise websocket frame limit, load .env, drop dead code

Running every learning example turned up three separate reasons a shipped
script could not work at all.

blockSubscribe examples were completely non-functional. websockets
defaults max_size to 1 MiB and Solana's blockSubscribe frames run well
past that, so the connection died with a 1009 close on the first real
block and the retry loop spun. listen_blocksubscribe.py logged 158,613
"message too big" errors in 35s and decoded zero tokens; compare_listeners
produced 10.1M error lines and its block column never reported anything.
Pass max_size=WEBSOCKET_MAX_MESSAGE_BYTES (32 MiB, the value the bot's own
listeners already use) at all 12 example connect sites — logsSubscribe and
programSubscribe included, since they have the same latent ceiling.

After: listen_blocksubscribe decodes tokens with 0 errors, and
compare_listeners reports provider_1_block alongside geyser and logs.

Seven examples read SOLANA_* from the environment but never called
load_dotenv(), so they only ran with variables already exported —
manual_buy, manual_buy_cu_optimized, manual_buy_geyser, manual_sell,
fetch_price, blockSubscribe_extract_transactions and
sample_cashback_pumpswap. manual_buy died on
"None isn't a valid URI: scheme isn't ws or wss" against a normal .env
checkout.

Dead code and artifacts:

- unused datetime import in listen_pumpportal
- initial_real_token_reserves computed and never read in mint_and_buy and
  mint_and_buy_v2
- gitignore blockSubscribe-transactions/, which a shipped example writes
  into 1200+ files deep and which nothing ignored
- stop tracking trades/trades.log; .gitignore has listed it all along, but
  a tracked file ignores .gitignore

ruff over src/ and learning-examples/ goes 2396 -> 2393 findings: the three
removals, nothing new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFrxcZfCf9C76voCb7M8Pa

* fix: read balances at confirmed, derive letsbonk platform_config per pool

Running every example against mainnet surfaced four more bugs, three of
them the same root cause as the commitment bug already in this branch:
solana-py defaults to "finalized", but trades confirm at "confirmed", and
finalization lags far enough behind that anything read in between is
pre-trade state.

get_token_account_balance defaulted to finalized. Cleanup reads it to
decide whether to burn before closing, so right after a sell it saw the
pre-sell amount and built a burn for tokens the account no longer held —
the burn + close reverted with InsufficientFunds and the rent stayed
locked. Observed live: "Burning 35766666 tokens" on an account the sell
had already emptied, then Custom(1). Defaults to confirmed now, which is
also what confirm_transaction uses.

manual_sell_pumpswap read the user's base balance at finalized, which
failed two ways in one session: "could not find account" when the ATA had
been created by a buy moments earlier, and a stale non-zero balance whose
transfer then reverted with insufficient funds. Pool vault reads move to
confirmed too, so quotes are not priced off stale reserves.

The letsbonk examples hardcoded platform_config. LaunchLab pools do not
share one — partner launches carry their own — so every buy/sell against
such a pool failed with ConstraintAddress (2012):

  AnchorError caused by account: platform_config
  Left:  5thqcDwKp5QQ8US4XRMoseGeGbmLKMmoKZmS6zHrQAsA
  Right: FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1

PoolState carries platform_config at field 18 and these scripts already
parse the pool, so they now take the pool's own value and keep the
constant only as a fallback. src/platforms/letsbonk already documented
this; only the examples were stale. All four scripts now simulate and land
(buy_exact_in, sell_exact_in, buy_exact_out, sell_exact_out).

They also discarded simulation logs on failure, printing an error number
with no indication of which account or constraint broke. They print the
program logs now — that is how the above was diagnosed.

Runnability and credentials:

- fetch_price, cleanup_accounts and the four letsbonk scripts hardcoded a
  placeholder ("...", "YOUR_TOKEN_MINT_ADDRESS_HERE") and could not run at
  all without editing the source. They take argv[1] now, matching
  manual_sell and the pumpswap scripts. cleanup_accounts takes "2022" as
  argv[2] for Token-2022 mints, which every pump.fun coin is.
- six scripts printed the RPC/WSS endpoint, which carries an API key, into
  stdout. They print only the host now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFrxcZfCf9C76voCb7M8Pa

* fix(cleanup): never burn wrapped SOL, and label what actually happened

learning-examples/cleanup_accounts.py burned any non-zero balance before
closing. The token program rejects a burn of native (wrapped) SOL with
NativeNotSupported (error 10), so the burn + close transaction reverted and
a WSOL account could never be cleaned up — the pumpswap examples leave one
behind on every sell. Closing a WSOL account already returns both the
wrapped lamports and the rent, so there is nothing to burn first.

src/cleanup/manager.py already had this guard; only the example was
missing it. Observed live: "Burning 2206381 tokens" then Custom(10), with
2.2m lamports stranded until the guard went in.

The success line also claimed "Burned and closed" for the unwrap path,
which burns nothing. It now reports Unwrapped/Burned/Closed to match the
instructions actually built.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFrxcZfCf9C76voCb7M8Pa

* chore: drop captured-output artifacts and a spent one-off investigation script

learning-examples/decoded_buy_tx_from_getTransaction.json and
decoded_create_tx_from_getTransaction.json are not JSON — they are captured
stdout from the decode scripts, several objects concatenated, so json.load
raises "Extra data" on both. Nothing reads them and nothing can. The
raw_*.json fixtures stay: all three are valid input and were re-checked
through decode_from_getTransaction / decode_from_blockSubscribe /
decode_from_getAccountInfo.

learning-examples/pumpswap/sample_cashback_pumpswap.py was added by #168 to
find the position of the extra account that cashback PumpSwap pools
require. That question is answered — the layouts are in
platforms/pumpfun/instruction_builder.py and machine-checked against the
IDL by verify_v2_account_layout.py. It is undocumented, unlike every other
pumpswap example in the README, and nothing imports it.

Not touched: logs/ (run history), .cursor/.kiro/.windsurf (deliberate
mirrors of the same rules for other editors), and the raw_*.json fixtures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFrxcZfCf9C76voCb7M8Pa

* fix(tx-status): fail closed on missing metadata, stop retrying landed reverts

Addresses the must-fix findings from review. Two of them were wrong in code
this branch added.

assert_transaction_succeeded folded meta=None into err=None and returned
success. Missing execution metadata means the outcome is unknown, not that
it succeeded — which is the exact mistake this module exists to prevent. It
raises now.

It also always read at "confirmed" while confirm_and_assert accepted a
commitment argument, so confirming at "finalized" and then reading status at
"confirmed" could report success before the finalization the caller asked
for. The commitment is threaded through.

The manual buy/sell retry loops built one message and blockhash before the
loop, so a landed revert was retried by resubmitting byte-identical signed
bytes — three attempts with backoff that could never succeed. Reverts now
raise TransactionRevertedError (a RuntimeError subclass, so existing
handlers keep working) and the loops treat it as terminal. Rebuilding and
re-signing per attempt is the fuller fix but a bigger change to these
scripts than this branch should carry.

cleanup_accounts guessed the mint's token program, defaulting to legacy SPL
unless argv[2] was "2022". The ATA address differs between programs, so a
wrong guess derives an address that does not exist and the script reports
"already closed" for an account it never looked at. It reads the owner off
the mint account instead, which is authoritative, and rejects anything not
owned by a token program. Verified live: pump.fun coins resolve to
Token-2022, letsbonk and USDC to legacy. argv[2] is gone — nothing to guess.

urlsplit(...).netloc keeps any user:pass@ userinfo, so the endpoint
redaction added earlier still printed credentials for providers that put the
key there. Uses .hostname in all five sites.

Four checks added to verify_tx_status_checks.py, each mutation-tested:
missing metadata, revert-is-terminal, commitment propagation, and a scan
that fails if any example goes back to netloc.

Skipped, both minor and pre-existing repo-wide rather than introduced here:
validating that RPC/WSS env vars are non-None before connecting (every
example has this shape; fixing two of them would just make it inconsistent),
and the cryptic base58 error when a letsbonk script runs with no argument,
which matches the "..." placeholder convention the pumpswap and manual_sell
examples already use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFrxcZfCf9C76voCb7M8Pa

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 13:44:35 +02:00
02343b775b feat(pumpfun): migrate to buy_v2/sell_v2 and support non-SOL quote assets (#176)
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>
2026-07-28 17:58:33 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3b88a06d9d build(deps-dev): bump setuptools in the uv group across 1 directory (#174)
Bumps the uv group with 1 update in the / directory: [setuptools](https://github.com/pypa/setuptools).


Updates `setuptools` from 80.9.0 to 83.0.0
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.9.0...v83.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 83.0.0
  dependency-type: direct:development
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 15:29:38 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8d561dd421 build(deps): bump idna in the uv group across 1 directory (#173)
Bumps the uv group with 1 update in the / directory: [idna](https://github.com/kjd/idna).


Updates `idna` from 3.10 to 3.15
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.10...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-29 20:48:22 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9ab686b453 build(deps): bump python-dotenv in the uv group across 1 directory (#166)
Bumps the uv group with 1 update in the / directory: [python-dotenv](https://github.com/theskumar/python-dotenv).


Updates `python-dotenv` from 1.1.1 to 1.2.1
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.1.1...v1.2.1)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-version: 1.2.1
  dependency-type: direct:production
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 22:45:44 +02:00
Anton SauchykandClaude Opus 4.7 27929e97be perf(extreme-fast-mode): use processed commitment + tight retry for BC refresh
Before: BC fetch in extreme_fast_mode used default (confirmed) commitment
and retried up to 10 times with growing backoff (~10s total). Geyser/logs
fire on processed, so the BC isn't yet at confirmed commitment when the
bot reads it — most fetches failed and burned the full retry budget,
adding ~10s to detect→confirmed.

After: read at processed commitment to match the listener event horizon,
and cap retries at 4 with 150ms gaps (~600ms ceiling). Most reads succeed
on the first attempt.

Mainnet benchmark (n=4 successful buys, geyser listener):
  buy_slot − create_slot: min=2  median=3  max=9  (~0.8s — 3.6s on chain)
  wall detect→confirmed:  median 2-3s (1s resolution)

Best case 2 slots = next slot after creation + tx propagation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:11:37 +02:00
Anton SauchykandClaude Opus 4.7 20e3e163d8 fix(trading): refresh BC state on extreme_fast_mode buy + letsbonk pumpportal token-program default
Two related fixes from mainnet validation of geyser/logs/blocks/pumpportal:

1. extreme_fast_mode buys previously skipped the BC fetch entirely. After
   the 2026-04-28 cutover, fee_recipient (mayhem mode) and creator_vault
   (PFEE-delegated BC.creator) cannot be derived from listener-side data
   alone — the program rejects with NotAuthorized (0x1770) /
   ConstraintSeeds (0x7d6). Add a short retry loop that fetches the curve
   state once before submitting the buy, so the instruction builder picks
   the correct fee_recipient and creator_vault. The ~150ms cost is small
   relative to a failed tx and 5s sell wait.

2. LetsBonkPumpPortalProcessor returned TokenInfo without token_program_id.
   The universal builder then defaulted to Token-2022 and the buy failed
   with IncorrectProgramId at GetAccountDataSize on the user-ATA create
   ix. LetsBonk tokens are predominantly regular SPL Token, so default to
   TOKEN_PROGRAM in the processor.

Note: the pumpportal listener still has unrelated data-race issues (the
PumpPortal stream sometimes fires before the on-chain BC is RPC-readable,
and bondingCurveKey occasionally references a different mint than the
detected token). These predate the 2026-04-28 cutover and need a separate
fix (e.g., verifying mint+BC consistency on the tx side before submit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:12:37 +02:00
Anton SauchykandClaude Opus 4.7 2ece1c9a2d fix(trading): refresh creator_vault from on-chain BC state before sell
Post-2026-04-28, BC.creator can be delegated to a PFEE-program-owned PDA
after the initial creator buy. The create-time creator_vault cached on
TokenInfo goes stale before the sell lands, producing ConstraintSeeds
(0x7d6) on the Sell instruction.

The sell flow already re-fetches pool_state to refresh is_mayhem_mode and
is_cashback_coin; extend it to also refresh token_info.creator and re-derive
creator_vault from the current BC.creator.

Mainnet validation: geyser listener buy+sell on BEE — buy 2TSqZHZh…k7uN,
sell 3TevcP5g…f8nQ, err=None on both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:51:56 +02:00
Anton SauchykandClaude Opus 4.7 d0dea6d4ac fix(blocks-listener): skip failed txs + use CreateEvent log for canonical creator
Two bugs found by mainnet validation post-2026-04-28 cutover:

1. Blocks listener processed failed txs, including failed create_v2s. The
   bot then tried to buy a non-existent mint (InvalidMint at ATA creation).
   Fix: filter `meta.err is not None` in _process_block_transactions.

2. Blocks listener parsed `args.creator` from the create_v2 ix payload to
   derive creator_vault. Post-cutover, BC.creator is sometimes set to a
   PFEE-program-owned PDA distinct from args.creator (e.g., Companions:
   args.creator=DdZG8dw, BC.creator=3hPZm9). The Anchor seeds constraint
   on creator_vault then fails with ConstraintSeeds (0x7d6). Fix: in
   parse_token_creation_from_block, prefer parsing the CreateEvent log
   (which carries the canonical creator the program wrote to BC.creator)
   over args.creator from the ix payload.

Mainnet validation:
- buy 2uBrNqqW…bPDG, sell 32EKJjrK…9fXv on PUMP (Pumpcoin), err=None.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:44:42 +02:00
cc85daaeb3 chore(pumpswap): post-2026-04-28 cutover + cashback layout (#168)
* chore(pumpswap+docs): post-2026-04-28-cutover PumpSwap unlock + IDL docs

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

Also captures the protocol gotchas we learned during this migration:

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:20:53 +02:00
22a0c23450 feat(pumpfun): align src/ with 2026-04-28 program upgrade + cashback path (#167)
Mirrors the four learning-examples commits (9b48418, c25e2ed, 57ffe8b,
ad6d2cc) into the production code paths under src/. Affects only pump_fun;
lets_bonk untouched. PumpSwap is not implemented under src/ so the
pool-v2 + breaking-fee-recipient pumpswap accounts are out of scope here.

Changes
-------
src/platforms/pumpfun/address_provider.py
  - Adds BREAKING_FEE_RECIPIENTS: ClassVar[list[Pubkey]] with the 8
    addresses pump.fun published for the 2026-04-28 upgrade.
  - Adds pick_breaking_fee_recipient() (random.choice across the 8 to
    spread program-tx throughput per pump.fun's recommendation).
  - get_buy_instruction_accounts and get_sell_instruction_accounts now
    expose "breaking_fee_recipient" alongside the existing keys.

src/platforms/pumpfun/instruction_builder.py
  - BC buy ix: appends breaking_fee_recipient (mutable) AFTER
    bonding_curve_v2 → 18 accounts total (was 17).
  - BC sell ix: appends breaking_fee_recipient (mutable) AFTER
    bonding_curve_v2 for both cashback and non-cashback paths
    → 16 accounts non-cashback / 17 cashback (was 15 / 16).
  - get_required_accounts_for_{buy,sell} updated to include the new
    account so priority-fee scraping covers it.

src/platforms/pumpfun/event_parser.py
  - is_mayhem_mode now extracted into TokenInfo on both paths
    (CreateEvent logs path and create / create_v2 instruction path);
    previously only is_cashback_coin was carried forward, leaving
    extreme_fast_mode buys to use the wrong fee_recipient on
    Mayhem-Mode tokens.

src/trading/platform_aware.py
  - Buy non-extreme path now also refreshes is_cashback_coin from
    pool_state (was only refreshing is_mayhem_mode).
  - Sell path now refreshes both flags from curve state before building
    the sell ix, so a coin that flips cashback after buy still gets the
    correct 16-vs-17-account layout. Wrapped in try/except so a transient
    RPC failure logs a warning and falls back to token_info defaults
    rather than failing the sell outright.

Live-validation (mainnet, all four bots/*.yaml configs)
-------------------------------------------------------
  bot-sniper-2-logs.yaml      buy 4GVJJwYtGixSPAQimgUhNYjEUBG6TB9XeQjKxs9EWpw2QfWuq7LHmUivPrsjhESgaPsPJQ9N6CCxECqxm9csqrH9 (18 accts) → sell 2ijgoPgxPmcSX4mFqz1PVkZ3yAvu36vj3Jy1yWZofxrK7jR92j2DWP3tRNy9PmUd1kiTaoYzVsrpGr6286KCnTp7 (16 accts) ✓
  bot-sniper-3-blocks.yaml    buy 4siRLG7tYk9iHyXAvnp3DH54vQXVEDHmCqZRmFgYhnRB9mVGjhh43ywohKjg8CpWctt5TyCtDkLRTRWT6VDwVyZi → sell 46jShWJ3sfuCP4YgTcpXWZdyqVmw4ULQSTC3Kwvbc9hE2y4yDosBww8Un8anXZpR1Xf96rN9QusV1G92oNcbp1o3 ✓
  bot-sniper-1-geyser.yaml    buy 4UEZy7LPm7Rxgw22d1bduQHTWHHHYJSpuaoq2iCyPFpnBPGt4C3QUCu9x77vwWugXnyGv4pQQeTbEZEjvyxxiDH2 → sell 21hSHVPKtqse25XybxhdGrpqK1fSxKKeNZGDCD5j568mzRPfyYR1TQs6mAc2moHKT7kaAAfdMC8DPt8VXmu2rviF ✓
  bot-sniper-4-pp.yaml        buy 4P91y4iPK6y1gPjz65VL6XAcVkT4DbqjHE5vHfX8PLnzqViCy1qZQ44eWiNDUPzoWCejibB7UJPKYAEj4K3Z3c3S ✓ ; sell hit slippage protection (Custom 6003 — program-side, the ix passed deserialization)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:39:17 +02:00
Anton SauchykandClaude Opus 4.7 bda9099275 chore(examples): accept argv override and bump RPC timeouts
Three small ergonomic fixes uncovered while running every learning
example end-to-end:

  copytrading/listen_wallet_transactions.py
    Was hardcoded to WALLET_TO_TRACK = "...". Now accepts argv[1] so
    you can run it without editing the file.

  pumpswap/get_pumpswap_pools.py
    Same pattern (TOKEN_MINT placeholder) and the underlying
    getProgramAccounts call would also time out at the default 30s
    on busy mainnet RPCs — bumped both AsyncClient timeouts to 120s.

  bonding-curve-progress/get_graduating_tokens.py
    Bumped RPC timeout 120s → 240s for the same reason. The threshold
    scan over all pump.fun bonding curves still occasionally exceeds
    even that on overloaded RPCs (it's a known-heavy query); a smaller
    threshold or memcmp-narrowed filter is the proper long-term fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:52:41 +02:00
Anton SauchykandClaude Opus 4.7 ad6d2cc755 fix(examples): add pool-v2 account to pumpswap, repair geyser listener
PumpSwap (manual_buy_pumpswap.py / manual_sell_pumpswap.py)
  - Real on-chain pump-swap buys use 24 accounts; the IDL is incomplete
    (lists 23). Missing 24th account is pool-v2 PDA, derived from seed
    ["pool-v2", base_mint] under the pump-amm program.
  - Without it, pump-amm throws AnchorError 6023 (Overflow) at buy.rs:400/414
    after the trade transfers complete (the source of the earlier "pre-existing
    overflow" — was actually a missing account, not a math bug).
  - find_pool_v2(base_mint) added; appended to both buy and sell account lists
    after fee_program. The April-28 breaking-fee accounts (still gated behind
    INCLUDE_BREAKING_FEE_ACCOUNTS) come after pool-v2 — matching the doc's
    "All accounts till bonding-curve-v2 and pool-v2 remain the same."
  - AsyncClient timeout bumped to 120s so the heavy getProgramAccounts pool
    lookup doesn't time out on slower RPC paths.
  - Live-verified on mainnet with 9PwadsGz...pump:
      buy   2xgdJJsDMzoMyxJzrCATKwGfpX7M... err=None (24 accounts)
      sell  4vdBkGpCMQYEWncj5k1suGzKPUMQ... err=None (24 accounts)

manual_buy_geyser.py
  - Fixes IndexError when create tx references ALT-loaded keys
    (matches the same fix applied to manual_buy.py).
  - Re-enables the 15s sleep + on-chain price/fee_recipient lookup that
    were commented out for testing — needed for buys on real pools.
  - Live-verified: 45wSgX2Pu1LLZWLgu3e9qdhRaGJjqwMqQP5HdeV7zMBqNXziB2uEgxQd5bAqvAhBLFnAqzbuK3LAWowhxZxdemJ
    (18-account buy, err=None) on Token-2022 cashback-enabled INSIDER mint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:35:13 +02:00
Anton SauchykandClaude Opus 4.7 57ffe8bfbb feat(examples): apply 18-account buy upgrade to mint_and_buy + pumpswap
Extends the 2026-04-28 program-upgrade prep (commit c25e2ed) to the
remaining trade scripts:
  mint_and_buy.py              — 18-account buy after bonding-curve-v2.
                                 Tested live with a fresh TEST mint (Solscan
                                 sig 62EZFCKVuxvqmzj...), succeeded.
  mint_and_buy_v2.py           — 18-account buy. Tested live with a
                                 mayhem-mode TEST2 token (Token-2022 + 5
                                 mayhem accounts), Solscan sig 44SvQSrhmHy...,
                                 succeeded.
  pumpswap/manual_buy_pumpswap.py / manual_sell_pumpswap.py
                               — Adds the 2 new pump-swap accounts (fee
                                 recipient + its quote-mint ATA) gated behind
                                 INCLUDE_BREAKING_FEE_ACCOUNTS = False; flip
                                 to True after 2026-04-28 16:00 UTC.
                                 Pre-existing program-side overflow at
                                 buy.rs:400 (AnchorError 6023) flagged in
                                 a comment — affects current pump-swap
                                 buys, unrelated to this upgrade.

Both pumpswap scripts now also accept TOKEN_MINT via argv[1].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:09:14 +02:00
Anton SauchykandClaude Opus 4.7 c25e2edb57 feat(examples): add 18-account buy / 16-17-account sell for 2026-04-28 upgrade
pump.fun program upgrade scheduled for 2026-04-28 16:00 UTC adds one new
mutable account (one of 8 fee recipients) at the end of buy/sell ixs,
after bonding-curve-v2. The new format is already accepted on mainnet
ahead of the cutover, so applying now keeps scripts working on both
sides of the upgrade.

Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md

Per file:
  manual_buy.py            — 18-account buy. Also fixes a pre-existing
                             listener crash on Address-Lookup-Table create
                             txs (skips them) and adds bool/OptionBool
                             handling to decode_create_instruction so
                             create_v2 doesn't raise on its trailing args.
  manual_buy_cu_optimized.py — 18-account buy. Bumps the CU-optimization
                             account-data limit to 16MB (tested) — older
                             values trigger MaxLoadedAccountsDataSizeExceeded
                             on Token-2022/cashback coins. Same listener
                             ALT-skip and decoder bool fixes.
  manual_buy_geyser.py     — 18-account buy. Re-enables price calculation
                             and mayhem-mode-aware fee_recipient detection
                             (was scaffolded out for testing).
  manual_sell.py           — 16-account non-cashback / 17-account cashback
                             sell. Reads is_cashback_coin from BC byte 82
                             and inserts user_volume_accumulator before
                             bonding-curve-v2 when needed. TOKEN_MINT now
                             accepts argv[1] override.

Live-verified on mainnet:
  buy   manual_buy.py             3AVnLC3sdBD598cs3... + 3bWU9bB8U9aemA2BFhS2...
  buy   manual_buy_cu_optimized   2PfH5rHw62o3KkQ3N4z8...
  sell  manual_sell.py            3E7dyPBPBRe95BtPQXmb... + 5L4wWSzPV36m6XqXAswB...
All four show the expected 18 (buy) / 16 (sell non-cashback) account count
with one of the 8 BREAKING_FEE_RECIPIENTS as the trailing mutable account.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:43:31 +02:00
Anton SauchykandClaude Opus 4.7 9b4841890c fix(examples): align listeners and decoders with refreshed pump.fun IDL
Refreshes the read-only learning examples (Phase A3) against the post
Feb–Apr 2026 IDL changes:
  - CreateEvent now has 15 fields (timestamp, 4×u64 reserves, token_program,
    is_mayhem_mode, is_cashback_enabled). Listener parsers were truncating
    after `creator`; the v2 ones were also reading is_mayhem_mode at the
    wrong offset.
  - BondingCurve account is now 83 bytes (added is_cashback_coin).
  - PumpSwap Pool account is now 245 bytes (added is_cashback_coin) — the
    listen_programsubscribe dataSize filter was matching nothing.
  - IDL instruction `createV2` was renamed to `create_v2`; the legacy
    `create` instruction added a `creator: pubkey` arg in March 2026.
  - decode_from_blockSubscribe.py decoder now handles bool, i64, u32, u16,
    u8, and the OptionBool defined type (1-byte wire format).

Files touched (all under learning-examples/):
  bonding-curve-progress/get_bonding_curve_status.py  — V4 struct (cashback)
  bonding-curve-progress/poll_bonding_curve_progress.py
  decode_from_blockSubscribe.py
  decode_from_getAccountInfo.py
  decode_from_getTransaction.py
  listen-migrations/compare_migration_listeners.py
  listen-migrations/listen_programsubscribe.py
  listen-new-tokens/compare_listeners.py
  listen-new-tokens/listen_blocksubscribe.py
  listen-new-tokens/listen_geyser.py
  listen-new-tokens/listen_logsubscribe.py
  listen-new-tokens/listen_logsubscribe_abc.py

Verified live against mainnet — listeners decode tokens including
mayhem-mode and cashback-enabled curves correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:42:11 +02:00
Anton SauchykandClaude Opus 4.7 156cf508a0 chore(idl): refresh pump.fun IDLs from upstream
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>
2026-04-27 16:14:46 +02:00
Anton SauchykandClaude Opus 4.7 7864504c84 docs(idl): vendor upstream pump.fun IDLs for reference
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>
2026-04-27 16:11:19 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4290b54f31 build(deps): bump aiohttp in the uv group across 1 directory (#164)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.13.4
  dependency-type: direct:production
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-12 22:11:50 +02:00
smypmsa 02692793a9 docs: add cli and skill links 2026-03-20 18:56:23 +00:00
smypmsaandClaude Sonnet 4.6 73fe95efe4 docs: mention OpenClaw in pumpclaw ecosystem note
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 18:52:56 +00:00
smypmsaandClaude Sonnet 4.6 f8eb5eb8a0 docs: add ecosystem links to pumpfun-cli and pumpclaw
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 18:51:54 +00:00
546b94af6f fix(examples): add bonding_curve_v2 to learning examples (#160)
* fix(examples): add bonding_curve_v2 remaining account to learning examples

Add the required bonding_curve_v2 PDA as a remaining account to buy/sell
instructions in manual_buy, manual_sell, mint_and_buy, and mint_and_buy_v2
learning examples, matching the pump.fun program upgrade.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(examples): add bonding_curve_v2 to manual_buy_geyser and manual_buy_cu_optimized

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 23:54:18 +01:00
4a48f545c1 feat(pumpfun): update IDLs and add bonding_curve_v2 + cashback support (#159)
Update all 3 pump.fun IDL files (pump_fun, pump_swap, pump_fees) with
new cashback rewards and fee-sharing features. Fix IDL parser to handle
tuple struct types (OptionBool). Add bonding_curve_v2 remaining account
to all buy/sell instructions as required by the pump.fun program upgrade.

Key changes:
- Fix IDL parser crash on tuple struct fields (string vs dict)
- Add bonding_curve_v2 PDA derivation and append as remaining account
- Add is_cashback_coin field to TokenInfo and BondingCurve decoding
- Propagate is_cashback_enabled from CreateEvent/create_v2 to TokenInfo
- Conditionally include user_volume_accumulator for cashback sell txs

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 23:39:52 +01:00
0b6779ca34 Fix buy/sell transactions silently failing on-chain (#157)
* fix(core): disable account_data_size limit and add meta.err checking

The 12.5MB setLoadedAccountsDataSizeLimit was causing all buy transactions
to fail with MaxLoadedAccountsDataSizeExceeded since pump.fun migrated to
Token-2022. Transactions landed on-chain (fees paid) but inner instructions
were rejected — the bot paid gas for nothing.

Changes:
- Disable account_data_size in all bot configs (Token-2022 needs >12.5MB)
- Add meta.err check in get_buy_transaction_details() for clear error
  reporting when transactions fail on-chain
- Include tx signature in platform_aware.py error messages for debugging

Tested: full buy→sell→cleanup cycle works on pump_fun with geyser listener.


* fix(core): check meta.err in confirm_transaction to detect failed txs

Solana transactions can be "confirmed" (included in a block) but still
fail execution if inner program instructions are rejected. Previously,
confirm_transaction only checked that the tx landed on-chain, causing
silent failures in buy, sell, and cleanup operations.

Now fetches the transaction result after confirmation and checks
meta.err, returning False when the transaction failed. This fixes
the ATA cleanup issue where sells were silently failing with
Custom: 6003 errors, leaving non-zero token balances.


* fix(core): treat failed tx fetch as unconfirmed in confirm_transaction

When _get_transaction_result returns None (RPC failure, timeout, etc.),
the function was falling through to return True — silently treating an
unknown state as success. Now returns False with a warning log.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:37:04 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c5b0161d05 build(deps): bump the uv group across 1 directory with 2 updates (#156)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.13.3
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: protobuf
  dependency-version: 6.33.5
  dependency-type: direct:production
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 10:20:46 +01:00
40ad32b8d0 feat(core): add RPC rate limiting and retry handling (#154)
* 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>
2026-02-17 10:02:11 +01:00
31955103dc feat: add Windows compatibility for uvloop dependency (#155)
Fixes chainstacklabs/pumpfun-bonkfun-bot#152

Changes:
- Make uvloop optional with platform-specific markers (Unix only)
- Add winloop as Windows alternative for performance optimization
- Update code to gracefully fall back to standard asyncio when event loop
  libraries are unavailable
- Both bot_runner.py and universal_trader.py now detect platform and use
  appropriate event loop implementation

This resolves the blocking installation issue for Windows users while
maintaining performance benefits on all platforms. Windows users can now
install and run the bot with winloop for improved performance, or use
standard asyncio as fallback.

Related: https://github.com/smypmsa/gh-triage-reports/blob/main/issues/chainstacklabs-pumpfun-bonkfun-bot/issue-152-2026-02-08.md

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 16:58:24 +04:00