CLI for trading, launching, and browsing tokens on pump.fun (Solana). Python 3.12+, uv, Typer, solana-py/solders.
# Install / sync
uv sync
# Run CLI (dev)
uv run pumpfun <command> [options]
uv run pumpfun --json <command> # force JSON output
# Unit tests (always safe, no network)
uv run pytest tests/ -q
uv run pytest tests/ -v --cov # with coverage
uv run pytest tests/test_core -v # single module
uv run pytest tests/ -k test_name # filter
# Surfpool integration (needs running surfpool daemon)
./scripts/surfpool-autodiscover.sh
# E2e mainnet (COSTS REAL SOL β always confirm with user first)
./scripts/mainnet-test.sh
./scripts/mainnet-test.sh --skip-tradingpytest is configured to auto-ignore tests/test_surfpool/ by default. Surfpool and mainnet tests require explicit invocation.
Three-layer separation β never bypass layers:
commands/ β core/ β protocol/
(thin) (logic) (Solana)
commands/β Typer CLI wiring only. Parse args, call core, callrender()orerror(). All commands are async, bridged withasyncio.run().core/β Framework-free business logic. Accept primitives (rpc_url: str,password: str, amounts). Returndictwith results or"error"key for expected failures.protocol/β Pure Solana/pump.fun code. PDAs, RPC client, instruction builders, curve math, pool parsing. Zero business logic.
Entry point: src/pumpfun_cli/cli.py β pyproject.toml [project.scripts] pumpfun = "pumpfun_cli.cli:app"
src/pumpfun_cli/
βββ cli.py # Root Typer app, GlobalState, callback
βββ crypto.py # AES-256-GCM wallet encryption (scrypt KDF)
βββ output.py # render() + error() β TTY-aware output
βββ commands/ # Thin CLI layer (config, info, launch, tokens, trade, tx_status, wallet)
βββ core/ # Business logic (config, info, launch, pumpswap, tokens, trade, tx_status, wallet)
βββ protocol/ # Solana primitives (address, client, contracts, curve, idl_parser, instructions, pumpswap)
tests/
βββ test_commands/ # CLI smoke tests
βββ test_core/ # Mocked business logic tests
βββ test_protocol/ # Unit tests for protocol math/parsing
βββ test_surfpool/ # Integration tests (ignored by default)
Output: Use render(data, json_mode) for all output β auto-detects TTY (Rich table) vs pipe (JSON). Use error(msg, hint, exit_code) for failures β prints to stderr and raises SystemExit. Never use print().
Error handling: error() raises SystemExit β code after it is unreachable. Core functions return dict with "error" key for expected failures (graduated tokens, not found, slippage exceeded, insufficient_balance). Catch ValueError for wrong wallet password in every command that decrypts. Buy/sell functions perform pre-trade balance validation β SOL balance for buys (including fees + ATA rent), token balance for sells with specific amounts.
Imports: stdlib β third-party β local. Example:
import asyncio
from pathlib import Path
from solders.pubkey import Pubkey
from pumpfun_cli.core.config import resolve_value
from pumpfun_cli.output import render, errorNaming: snake_case functions/variables/files. UPPER_CASE constants. Prefix private helpers with _.
Type hints: Full type hints on all public function signatures.
Async: All I/O functions are async def. Commands bridge with asyncio.run(). RpcClient is stateless β always call .close() in finally.
Config resolution: resolve_value(key, env_var, flag) β flag > env var > config file > default.
- Add
core/my_feature.pyβasync def my_operation(rpc_url, keystore_path, password, ...) -> dict - Add
commands/my_feature.pyβ Typer wrapper that calls core and renders output - Register in
cli.pywithapp.command("my-command")(my_feature_cmd) - Add tests in
tests/test_core/test_my_feature.pywith mocked RPC/HTTP
# Derive PDA
from pumpfun_cli.protocol.address import derive_bonding_curve
# Fetch and decode
data = await client.get_account_data(address)
state = idl.decode_account_data(data, "BondingCurve", skip_discriminator=True)
# Build instructions
from pumpfun_cli.protocol.instructions import build_buy_ix
ixs = build_buy_ix(...)
sig = await client.send_tx(ixs, [keypair], compute_units=..., confirm=True)PUMPFUN_RPC=https://... # Solana RPC endpoint (required for trading)
PUMPFUN_PASSWORD=... # Wallet password (for non-interactive use)Config file: ~/.config/pumpfun-cli/config.toml
Wallet keystore: ~/.config/pumpfun-cli/wallet.enc
- Test wallet pubkey:
2kPYzWkeJCiUEpo7yBNX7jYdwmyqXGrKsjetNJdHPfYz(password:testpass123) - Test token mint:
72xpy6cejkorDh8gx328CAW3Fq7uCQdCyXkSLAE5to5p(CLITEST, on bonding curve)
Three hooks run automatically during Claude Code sessions (see .claude/settings.json):
- guard.py (PreToolUse β Write|Edit): Blocks edits to
.env,wallet.enc,idl/, and credential files (.pem,.key,.secret). Edit these files manually. - lint.py (PostToolUse β Write|Edit): Runs
ruff format+ruff check --fixon every edited.pyfile. If unfixable errors remain, you will see them as errors β fix them before moving on. - bash-guard.py (PreToolUse β Bash): Advisory only β prints warnings (never blocks) when commands send Solana transactions, run mainnet tests, or delete sensitive files. The CLI must work unimpeded when run via assistants or automated workflows.
Always do:
- Run
uv run pytest tests/ -qafter code changes - Follow the three-layer separation
- Use
render()anderror()for all CLI output - Catch
ValueErroron wallet decryption - Close
RpcClientinfinallyblocks
Ask first:
- Running mainnet e2e tests (costs real SOL)
- Modifying
protocol/contracts.pyconstants (program IDs, discriminators) - Changing wallet encryption format in
crypto.py
Never do:
- Bypass layers (commands must not import from protocol directly)
- Use
print()instead ofrender()/error() - Commit
.envor wallet files - Run mainnet tests without explicit user confirmation
- Add dependencies without discussing first