Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ spends that asset, so the wallet has to hold it.
## Commands

```
Token discovery (zero config):
pumpfun tokens trending [--limit N] Top runners + recommended
Token discovery (zero config; price and progress added when an RPC is set):
pumpfun tokens trending [--limit N] pump.fun's movers board
pumpfun tokens new [--limit N] Recently launched
pumpfun tokens graduating [--limit N] Near bonding curve completion
pumpfun tokens recommended [--limit N] Recommended by pump.fun
pumpfun tokens search <query> Search by keyword
pumpfun tokens graduating [--limit N] Closest to bonding curve completion
pumpfun tokens recommended [--limit N] Recommended by pump.fun (at most 10)
pumpfun tokens search <query> Search by name or ticker

Token info (requires RPC):
pumpfun info <mint> Price, bonding progress, reserves
Expand Down Expand Up @@ -134,11 +134,12 @@ Global options:

| Command | Endpoint | Protocol |
|---------|----------|----------|
| `tokens trending` | `frontend-api-v3.pump.fun/coins/top-runners` + `/coins/recommended` | HTTPS |
| `tokens new` | `frontend-api-v3.pump.fun/coins/recommended` | HTTPS |
| `tokens graduating` | `frontend-api-v3.pump.fun/coins/recommended` | HTTPS |
| `tokens recommended` | `frontend-api-v3.pump.fun/coins/recommended` | HTTPS |
| `tokens search` | `frontend-api-v3.pump.fun/coins/recommended` (client-side filter) | HTTPS |
| `tokens trending` | `frontend-api-v3.pump.fun/coins/boards/movers` | HTTPS |
| `tokens new` | `frontend-api-v3.pump.fun/coins/boards/new` | HTTPS |
| `tokens graduating` | `frontend-api-v3.pump.fun/coins/boards/graduating` | HTTPS |
| `tokens recommended` | `frontend-api-v3.pump.fun/coins/recommendations` | HTTPS |
| `tokens search` | `frontend-api-v3.pump.fun/coins/search-unrestricted` | HTTPS |
| `tokens *` with an RPC set | + Solana RPC (`getMultipleAccounts` for the bonding curves) | JSON-RPC |
| `info` | Solana RPC (`getAccountInfo` for bonding curve PDA) | JSON-RPC |
| `buy` | Solana RPC (`getAccountInfo`, `getLatestBlockhash`, `sendTransaction`) | JSON-RPC |
| `sell` | Solana RPC (`getAccountInfo`, `getTokenAccountBalance`, `getLatestBlockhash`, `sendTransaction`) | JSON-RPC |
Expand All @@ -154,6 +155,11 @@ Global options:
| `wallet create/show/import/export` | None (local keystore only) | — |
| `config *` | None (local config file only) | — |

The `tokens` listings use pump.fun's frontend API, which is unofficial and
changes without notice. They are narrowed to pump.fun coins on Solana; the API
no longer returns curve reserves, so price and bonding progress come from the
chain and need an RPC endpoint.

## Security

- Private keys are encrypted at rest (AES-256-GCM + scrypt)
Expand Down
21 changes: 10 additions & 11 deletions scripts/mainnet-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,12 @@ echo " Done. ($PASS pass)"
# --- Group 3: Token Discovery ---

echo "=== Group 3: Token Discovery ==="
run_json_array "Discovery" "tokens trending" "uv run pumpfun tokens trending --limit 3"
run_json_array "Discovery" "tokens trending" "uv run pumpfun tokens trending --limit 20"
TRENDING=$(last_output)
run_json_array "Discovery" "tokens new" "uv run pumpfun tokens new --limit 10"
NEW_TOKENS=$(last_output)
run_json_array "Discovery" "tokens graduating" "uv run pumpfun tokens graduating --limit 3"
run_json_array "Discovery" "tokens graduating" "uv run pumpfun tokens graduating --limit 10"
GRADUATING=$(last_output)
run_json_array "Discovery" "tokens recommended" "uv run pumpfun tokens recommended --limit 3"

# Search — known to sometimes return empty
Expand All @@ -207,11 +208,12 @@ fi

# --- Discover mints for later groups ---

ACTIVE_MINT=$(echo "$NEW_TOKENS" | python3 -c "
# A SOL-priced coin still on its curve; the graduating board has the deepest ones.
ACTIVE_MINT=$(echo "$GRADUATING" | python3 -c "
import json, sys
tokens = json.load(sys.stdin)
for t in tokens:
if not t.get('complete', True) and t.get('real_token_reserves', 0) > 0 and t.get('real_sol_reserves', 0) > 1_000_000_000:
if not t.get('complete', True) and t.get('quote_symbol') == 'SOL':
print(t['mint'])
break
else:
Expand All @@ -221,14 +223,11 @@ else:
GRADUATED_MINT=$(echo "$TRENDING" | python3 -c "
import json, sys
tokens = json.load(sys.stdin)
# Prefer graduated tokens with higher SOL reserves — pools with extreme
# base/quote ratios can trigger on-chain overflow at buy.rs:400.
# A graduated SOL-priced coin; the largest one has the deepest PumpSwap pool.
best = None
for t in tokens:
if t.get('complete') and t.get('pump_swap_pool'):
if best is None:
best = t
elif t.get('market_cap', 0) > best.get('market_cap', 0):
if t.get('complete') and t.get('quote_symbol') == 'SOL':
if best is None or (t.get('usd_market_cap') or 0) > (best.get('usd_market_cap') or 0):
best = t
if best:
print(best['mint'])
Expand Down Expand Up @@ -309,7 +308,7 @@ else
import json, sys
tokens = json.load(sys.stdin)
for t in tokens:
if t.get('complete') and t.get('pump_swap_pool'):
if t.get('complete') and t.get('quote_symbol') == 'SOL':
print(t['mint'])
" 2>/dev/null)

Expand Down
2 changes: 1 addition & 1 deletion src/pumpfun_cli/commands/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def info(
typer.echo(f"Quote reserves: {result['real_quote_reserves']:.4f} {symbol}")
if result.get("pumpswap_warning"):
typer.echo(f"Warning: {result['pumpswap_warning']}")
typer.echo(f"Creator: {result['creator']}")
typer.echo(f"Creator: {result['creator'] or '\u2014'}")
if result["is_mayhem_mode"]:
typer.echo("Mayhem mode: Yes")
if result["is_cashback_coin"]:
Expand Down
61 changes: 41 additions & 20 deletions src/pumpfun_cli/commands/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
import typer

from pumpfun_cli.core.tokens import (
TokenApiError,
enrich_from_chain,
get_graduating_tokens,
get_new_tokens,
get_recommended_tokens,
get_trending_tokens,
search_tokens,
)
from pumpfun_cli.group import JsonAwareGroup
from pumpfun_cli.output import render, render_table
from pumpfun_cli.output import error, render, render_table

app = typer.Typer(
help="Browse and search pump.fun tokens.", invoke_without_command=True, cls=JsonAwareGroup
Expand All @@ -29,7 +31,7 @@ def _tokens_callback(ctx: typer.Context):
TOKEN_COLUMNS = [
("name", "Name"),
("symbol", "Ticker"),
("price_sol", "Price (SOL)"),
("_price", "Price"),
("_progress", "Progress"),
("_mcap", "MCap"),
("mint", "Mint"),
Expand All @@ -46,46 +48,66 @@ def _fmt_mcap(mcap) -> str:
return f"${mcap:.0f}"


def _display_tokens(ctx: typer.Context, tokens: list[dict]):
def _list(ctx: typer.Context, fetch) -> None:
"""Fetch a listing, add chain data when an RPC is configured, and render it."""
state = ctx.obj
rpc = state.rpc if state else None

enrich_failed = False

async def run():
nonlocal enrich_failed
tokens = await fetch
if rpc:
try:
tokens = await enrich_from_chain(tokens, rpc)
except (RuntimeError, ValueError):
enrich_failed = True # the listing is still useful without chain data
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return tokens

try:
tokens = asyncio.run(run())
except TokenApiError as exc:
error(str(exc), hint="pump.fun's frontend API is unofficial and changes without notice.")
if render(tokens, state and state.json_mode):
return
if enrich_failed:
typer.echo("Could not read prices from chain; showing pump.fun's data only.", err=True)
for t in tokens:
price = t.get("price_sol")
t["price_sol"] = f"{price:.8f}" if price is not None else "\u2014"
price = t.get("price")
symbol = t.get("quote_symbol") or ""
if len(symbol) > 6:
symbol = f"{symbol[:4]}\u2026"
t["_price"] = f"{price:.4g} {symbol}" if price is not None else "\u2014"
progress = t.get("bonding_progress")
t["_progress"] = f"{progress}%" if progress is not None else "\u2014"
t["_mcap"] = _fmt_mcap(t.get("usd_market_cap") or t.get("market_cap"))
t["_mcap"] = _fmt_mcap(t.get("usd_market_cap"))
render_table(tokens, TOKEN_COLUMNS)


@app.command("trending")
def trending(ctx: typer.Context, limit: int = typer.Option(20, "--limit", "-n", min=1)):
"""Top runners + recommended tokens."""
tokens = asyncio.run(get_trending_tokens(limit=limit))
_display_tokens(ctx, tokens)
"""Coins moving most right now (pump.fun's movers board)."""
_list(ctx, get_trending_tokens(limit=limit))


@app.command("new")
def new(ctx: typer.Context, limit: int = typer.Option(20, "--limit", "-n", min=1)):
"""Recently launched tokens."""
tokens = asyncio.run(get_new_tokens(limit=limit))
_display_tokens(ctx, tokens)
_list(ctx, get_new_tokens(limit=limit))


@app.command("graduating")
def graduating(ctx: typer.Context, limit: int = typer.Option(20, "--limit", "-n", min=1)):
"""Tokens near bonding curve completion."""
tokens = asyncio.run(get_graduating_tokens(limit=limit))
_display_tokens(ctx, tokens)
_list(ctx, get_graduating_tokens(limit=limit))


@app.command("recommended")
@app.command("top", hidden=True)
def recommended(ctx: typer.Context, limit: int = typer.Option(20, "--limit", "-n", min=1)):
"""Recommended tokens by pump.fun."""
tokens = asyncio.run(get_recommended_tokens(limit=limit))
_display_tokens(ctx, tokens)
def recommended(ctx: typer.Context, limit: int = typer.Option(10, "--limit", "-n", min=1)):
"""Recommended tokens by pump.fun (at most 10)."""
_list(ctx, get_recommended_tokens(limit=limit))


@app.command("search")
Expand All @@ -94,6 +116,5 @@ def search(
query: str = typer.Argument(...),
limit: int = typer.Option(20, "--limit", "-n", min=1),
):
"""Search tokens by keyword."""
tokens = asyncio.run(search_tokens(query, limit=limit))
_display_tokens(ctx, tokens)
"""Search tokens by name or ticker."""
_list(ctx, search_tokens(query, limit=limit))
2 changes: 1 addition & 1 deletion src/pumpfun_cli/core/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async def get_token_info(rpc_url: str, mint_str: str, rpc_timeout: float = 30.0)
"real_quote_reserves": state["real_quote_reserves"] / 10**quote_decimals,
"virtual_quote_reserves": state["virtual_quote_reserves"] / 10**quote_decimals,
"virtual_token_reserves": state["virtual_token_reserves"],
"creator": str(state["creator"]),
"creator": str(state["creator"]) if state["creator"] else None,
"is_mayhem_mode": state["is_mayhem_mode"],
"is_cashback_coin": state["is_cashback_coin"],
"is_holder_reward": bool(state["is_holder_reward"]),
Expand Down
Loading
Loading