29 Commits
Author SHA1 Message Date
d8a0ff7fe5 feat(platforms,cookbook): add StonkFun on a shared Raydium LaunchLab base (#235)
letsbonk.fun and StonkFun now share one LaunchLab implementation that prices
from real reserves, trades any quote mint, spends exactly the buy amount, and
accounts for the curve fee and Token-2022 transfer fees. Cleanup harvests
withheld fees before closing an account.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:29:07 +02:00
b7aae5e333 chore(monitoring,platforms,tools): retire the pumpportal listener (#232)
* chore(monitoring,platforms,tools): retire the pumpportal listener

Remove the third-party feed from the bot, the cookbook and the listener
race. It sampled rather than covered the chain, and was the one listener
that could never set state_from_event, so every buy paid the pre-buy
curve read.

Rescope the two buy-path verifiers that used it as a stand-in for a
TokenInfo without state_from_event.

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

* style(trading): rewrap a docstring paragraph

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 08:47:31 +02:00
Anton SauchykandGitHub de7fd2aa90 docs: compact docs and docstrings, split CLAUDE.md per Anthropic guidance (#221) 2026-09-23 19:00:03 +02:00
2ba40c0771 feat(monitoring): add shreds listener for pre-execution token detection (#220)
* feat(monitoring): add shreds listener for pre-execution token detection

Adds a `shreds` listener backed by geyser's SubscribeDeshred, which delivers
transactions when entries form from shreds, before execution. pump.fun only;
bots/bot-sniper-5-shreds.yaml ships disabled.

With no TransactionStatusMeta there is no CreateEvent, so the listener decodes
the create instruction and resolves what the buy needs itself. It marks its
tokens state_from_event, because there is no curve to read before the create
executes: trading on the instruction or not trading at all are the only
options.

Two fixes to the shared instruction-decode path, which geyser and blocks also
reach when logs are absent:

- `user` is account 5 on create_v2, not 7. create_v2 drops the metaplex
  accounts legacy create carries, so index 7 is the token program and every
  address derived from the wallet was wrong.
- A holder-reward coin's creator is PDA(["holder-rewards", mint]). The program
  ignores args.creator on those coins and writes that PDA into
  BondingCurve.creator, so creator_vault must derive from it or the buy is
  rejected on a seeds constraint.

Subscribes on the mint authority rather than the program id: it appears in
create/create_v2 and in no trade instruction, which keeps the stream to
creations instead of every pump.fun transaction.

Verified live on mainnet with extreme_fast_mode on and off and both the
time_based and tp_sl exits, each ending with the wallet holding SOL only.

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

* docs: drop reported measurement statistics from prose

Removes sample counts, traffic percentages, latency percentiles, window
durations and "measured on <date>" claims from docstrings, comments, docs and
bot YAML. The fact each measurement supported is kept and stated plainly.

Numbers that are load-bearing rather than reporting stay: protocol and packet
limits, the CU figures used to tune compute budgets, and dates pinning on-chain
or dependency upgrades.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 17:39:33 +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
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
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
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
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
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
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
Anton SauchykandGitHub 9d93d859a8 Reduce RPC delays when selling (#150)
* feat(trading): enhance sell execution with token amount and price parameters to reduce RPC delays

* feat(trading): add method to fetch actual token balance after transaction to improve accuracy

* feat(trading): update account data size limit and enhance price calculations
2025-11-23 12:02:29 +01:00
Anton SauchykandGitHub b6928d1f5f feat(core): integrate account data size limit (#144)
* feat(core): integrate account data size limit in buyer and seller transaction

* refactor: format code for improved readability and consistency
2025-10-28 21:21:21 +01:00
smypmsa 9731b179cc feat(core): add configurable cu limits 2025-09-03 15:05:12 +00:00
smypmsa cb5c9ed1cd chore: pumpportal time based config example 2025-08-05 20:48:03 +00:00
smypmsa bf7fbaa8ab chore(core): update example for logs 2025-08-04 05:07:05 +00:00
smypmsa 9b6563cecc feat(core): add event idl parser 2025-08-02 19:19:20 +00:00
smypmsa bae03c3fab fix(core): block event processor account keys bytes 2025-08-02 16:41:23 +00:00
smypmsa 26c048c86a fix(core): platform listener check 2025-08-02 16:12:45 +00:00
smypmsa e7107751f5 fix(letsbonk): buy/sell instructions 2025-08-02 15:38:53 +00:00
smypmsa bb35101a8f wip(core): platform aware trading 2025-08-02 09:25:39 +00:00
Bukhtiiarov b74d8df0a9 chore: update generated protobuf files 2025-06-30 15:10:59 +03:00
smypmsa 984f43b757 fix: remove coderabbit syntax bugs 2025-06-15 20:12:54 +00:00
smypmsa 473b1bec70 feat: add simple tp and sl with price polling 2025-06-15 18:20:13 +00:00
smypmsa 185811d854 feat: add pump portal listener 2025-06-15 11:48:31 +00:00
smypmsa e41a5f4f4f feat(core): add support for creator fee update 2025-05-12 20:07:51 +00:00
smypmsa aa145dc018 feat: add geyser auth type, compare listeners for new tokens 2025-05-06 08:39:13 +00:00
smypmsa a36fab7352 fix: comments, minor fixes 2025-04-29 11:56:55 +00:00
smypmsa 40e4a4b130 feat: add support for multiple bots 2025-04-24 20:32:47 +00:00