DEV Community

Devil Scrapes
Devil Scrapes

Posted on

pump.fun's search parameter accepts any string and returns the same feed. We shipped without it.

Quick answer

pump.fun's coin-feed API exposes a searchTerm query parameter that looks exactly like a search filter and isn't one. We sent searchTerm=doge and searchTerm=zzznonsensetoken999 — one a real memecoin ticker, one a string that matches nothing — at frontend-api-v3.pump.fun/coins, and got back byte-identical top-5 results both times: NTDA, WOTF, Trump Digital Oil Fund, United States Dividend Fund, NTDA again. The parameter is accepted, the request returns 200, and the response has nothing to do with what you asked for. So the pump.fun New Token Listings Scraper ships with no search field at all — not a placeholder, not a "coming soon," a permanent, tested exclusion — and instead does one thing correctly: a deduplicated, disjoint-paginated feed of every new Solana token launch, newest-first, at $3.20 per 1,000 rows.

Why does leaving a feature out need its own explanation?

Because shipping the obvious version would have been easy, and wrong. searchTerm exists on the endpoint. It's the kind of parameter a scraper gets built around without a second thought — accept a query string in the Actor input, pass it straight through, done. The API answers 200 either way, with a full page of real-looking token records, so nothing about the response tells you the filter is fake. It would pass a manual smoke test. It would pass an automated test that only checks "did I get rows back." It would ship, and it would bill customers normally, on every search — for a dataset unrelated to what they searched.

We'd already paid for that exact mistake once, on a different Actor (Avito): a 200 full of real rows, a filter parameter that silently didn't filter, green gates the whole way through. So the standing rule for every new Actor now is to re-probe any filter-shaped parameter with a nonsense value before trusting it — a real query against a query designed to match nothing. If the result set doesn't change, the parameter doesn't work, however plausible the response looks.

# recon, not shipped code — the check that killed the searchTerm field
import httpx

BASE = "https://frontend-api-v3.pump.fun/coins"
real = httpx.get(BASE, params={"searchTerm": "doge", "limit": 5}).json()
fake = httpx.get(BASE, params={"searchTerm": "zzznonsensetoken999", "limit": 5}).json()

names_real = [c["name"] for c in real]
names_fake = [c["name"] for c in fake]
assert names_real != names_fake, "searchTerm is not filtering"  # this assertion FAILED
Enter fullscreen mode Exit fullscreen mode

That assertion failed. names_real == names_fake, five for five, on the nose. searchTerm is present in the API's query string parsing — it doesn't 400, it doesn't get ignored silently in a way that returns an empty page — it just doesn't do anything to the result set. That's a worse failure mode than a missing filter, because a missing filter tells you it's missing.

How do you stop the field from quietly coming back later?

You write the exclusion as a test, not as a comment. A prose warning in a spec file gets read once, by whoever wrote it, and forgotten by whoever touches the code eight months later under a different set of assumptions. So ActorInput carries a test that introspects the model's own fields against a forbidden set — search_term, searchterm, query, keyword, search — and fails the suite outright if any of them exist:

SEARCH_SHAPED_NAMES = {"search_term", "searchterm", "query", "keyword", "search"}

def test_no_search_shaped_field_exists() -> None:
    """REQ-11 — schema-level enforcement of the searchTerm exclusion."""
    forbidden_hits = SEARCH_SHAPED_NAMES & set(ActorInput.model_fields)
    assert not forbidden_hits, f"search-shaped field(s) found on ActorInput: {forbidden_hits}"
Enter fullscreen mode Exit fullscreen mode

The bar for reintroducing search on this Actor isn't "seems useful" — it's a fresh live re-probe that shows the result set actually changes with the query, plus an automated test asserting the returned names relate to what was searched, landed before the field goes back in the schema. Until that happens, this test is the thing standing between a future contributor's good intentions and a repeat of the exact bug we just avoided.

What does the feed actually get right?

Pagination that's provably not repeating itself. offset=0 and offset=20 on the same sort=created_timestamp&order=DESC query returned 20 mint addresses each, zero overlap — verified live, not assumed from the parameter names, and pinned down with a regression test so a future change that starts silently re-serving the same page fails CI instead of shipping quietly wrong. Every row is deduplicated by mint, the Solana token's own mint address, across the whole run — the one field in the response that's actually guaranteed unique and never reused.

The sort key is narrowed the same way the search field was excluded: only created_timestamp ships, because it's the only sort value recon actually proved the API honours. And the host matters more than it should — frontend-api.pump.fun, without the -v3, answers HTTP 530 and is simply dead. Point a client at the wrong subdomain and you get a clean-looking failure for a reason that has nothing to do with blocking or rate limits.

What we handle for you

  • 🔁 We retry with exponential backoff on 408/429/503 and transport errors — capped at 5 attempts, Retry-After honoured — before giving up on a page and moving on with what's already collected.
  • 🧩 We dedupe by mint across the whole run, not per page, so a shifting feed never hands you the same token twice.
  • 🧱 We fail loud on exhausted retries, and we finish clean on a genuinely empty result — a filtered run that matches nothing isn't an error, and a run that couldn't reach the feed at all isn't a silent success.
  • 🔍 We validate every record through Pydantic before it's written — a single malformed token in a page gets logged and skipped, not allowed to crash the run.

Pricing

$0.20 per run start plus $0.003 per deduplicated token row — $3.20 per 1,000 rows. A filtered run that matches nothing costs only the start fee.

Run the pump.fun New Token Listings Scraper on Apify — free trial credit, no card required.

FAQ

Can I search by token name or ticker?
No, deliberately. pump.fun's own searchTerm parameter accepts any string and returns the same generic feed regardless — we verified it live rather than ship a filter that looks like it works and doesn't.

What do I get instead?
The full newest-first (or oldest-first) feed of Solana token launches, deduplicated by mint address, with a client-side min_market_cap / max_market_cap filter applied to rows already fetched — an honest post-filter, not a fake API parameter.

Do I need a wallet or API key?
No — the feed is public and keyless. No auth, no proxy group required to reach it.


Built by Devil Scrapes. Sometimes the right feature is the one we tested, found broken, and didn't ship. 😈

Top comments (0)