* 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>
Refresh the vendored IDLs from pump-fun/pump-public-docs @ 9c82f61 and move all
pump.fun trading onto the v2 instruction interface. This is required, not
optional: legacy buy/sell cannot trade coins paired against a quote asset other
than SOL, and USDC is already whitelisted in the on-chain Global account.
Protocol changes absorbed:
- buy_v2 (27 accounts) / sell_v2 (26 accounts) replace the legacy instructions.
Every account is mandatory and the order is identical for all coins, so the
conditional cashback/mayhem account lists are gone. Legacy remains available
via PumpFunInstructionBuilder(use_legacy_instructions=True).
- BondingCurve is 151 bytes: virtual_sol_reserves -> virtual_quote_reserves,
real_sol_reserves -> real_quote_reserves, plus quote_mint at offset 83. Old
field names are kept as aliases so existing callers keep working.
- v2 instruction data drops the track_volume OptionBool; amounts are in the
quote mint's raw units rather than always lamports.
- create_v2 carries a non-SOL quote mint as optional remaining accounts 17-19,
and CreateEvent gained quote_mint, so extreme_fast_mode can resolve the quote
asset without an extra fetch.
USDC support: new trade.quote_amounts and filters.allowed_quote_mints config,
accepting "sol"/"usdc" aliases or raw mints. Amounts are per-quote-mint because
1 USDC and 1 SOL are not interchangeable. A coin whose quote mint has no
configured amount is skipped rather than traded at the wrong size, so SOL-only
configs are unaffected.
Bug fixes found while verifying:
- The logs and blocks listeners set no websocket max_size, so any frame over
1 MiB closed the connection with 1009 and the token in it was lost. Raised
to 32 MiB.
- PumpSwap priced against the raw quote vault balance, ignoring the new
Pool.virtual_quote_reserves (i128 at offset 245; live pools are 301 bytes).
Upstream's note that this field is 0 everywhere is out of date: a live pool
carries 17.58 SOL against a 148 SOL vault, a 10.15% price error.
- The seller read curve state once at confirmed commitment and silently fell
back to create-time values, risking a stale creator_vault and ConstraintSeeds.
It now retries at processed, matching the buyer.
- Account cleanup would burn wrapped SOL when force_burn was set, destroying
value that closing the account returns. WSOL is now closed without burning.
- The mint scripts treated a landed transaction as a successful one, so a
reverted buy printed as success. They now assert the on-chain result.
Compute unit limits retuned from mainnet measurements: buy 100k -> 180k,
sell 60k -> 120k. Mint-and-buy is no longer atomic, because create_v2 plus
buy_v2 exceeds the 1232-byte transaction limit; both mint scripts send two
transactions.
Adds learning-examples/pump_v2.py as one shared, standalone v2 toolkit for the
example scripts, and three verification scripts: an offline layout check
against the IDL, a no-funds mainnet simulation, and a live listener matrix that
buys, sells and closes the ATA per listener.
Verified on mainnet: all four listeners (geyser, logs, blocks, pumpportal) and
all eight example scripts completed a real buy, sell and ATA close, each
confirmed by reading the transaction result back rather than trusting
confirmation alone. The USDC path is verified structurally only; no USDC-paired
coin could be found on-chain to exercise it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(pumpswap+docs): post-2026-04-28-cutover PumpSwap unlock + IDL docs
Drops the `INCLUDE_BREAKING_FEE_ACCOUNTS = False` gate in the PumpSwap
learning examples so the +2 breaking-fee accounts (fee recipient
readonly + its quote-mint ATA mutable) are always appended after
`pool-v2`. Mainnet pump-amm rejects this format pre-cutover (verified
6023 Overflow), so this PR is intentionally **draft until 2026-04-28
16:00 UTC** when the cutover happens; mark it ready-for-review and live-
test then.
Also captures the protocol gotchas we learned during this migration:
- README: a new "2026-04-28 program upgrade" section + an explicit note
that the vendored IDL is incomplete (missing `bonding-curve-v2` and
`pool-v2` PDAs that the on-chain program actually requires) with
pointers to cross-check against on-chain txs.
- CLAUDE.md: a new "Pump.fun protocol notes" subsection summarising
the same gotchas plus the BC/Pool/CreateEvent layout details and the
extreme_fast_mode gotcha.
Open question (call out at review time, resolve post-cutover):
- BREAKING_FEE_RECIPIENT.md shows PumpSwap cashback account counts of
27 buy / 26 sell vs 26 / 24 non-cashback — the extra cashback account
seed/position isn't documented. Need to sample a real successful
cashback PumpSwap tx after cutover and add the branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pumpswap): wire cashback layout for post-2026-04-28 program upgrade
Cashback PumpSwap pools require extra writable accounts inserted before
pool-v2. Identified layouts by sampling on-chain post-cutover txs:
- buy: insert user_volume_accumulator_quote_ata (27 accounts vs 26)
ref tx 4JaWdExj…fvjK
- sell: insert user_volume_accumulator_quote_ata + user_volume_accumulator
(26 accounts vs 24)
ref tx 4ei1cJV7…NP3
Detect via pool account byte 244 (is_cashback_coin). Adds the standalone
sample_cashback_pumpswap.py used to reverse-engineer the layouts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
* 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>
* 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>