CoinGecko tracks thousands of cryptocurrencies in one place, and its market pages hold exactly the structured data that drives price tracking, portfolio tools, and research: the coin name and symbol, the current price, market cap, 24 hour trading volume, the 24 hour and 7 day price change, and the market-cap rank. For anyone watching a basket of coins, that public market data is the raw material, and copying it by hand across hundreds of rows is slow and goes stale the moment you finish.
This guide shows you how to extract cryptocurrency data from CoinGecko with Python. CoinGecko publishes an official public API, and for any production workload that is the path you should reach for first. The HTML walkthrough here is an educational fallback for fields or pages the free API tier does not cover, and it stays scoped entirely to public market data, which is factual, not personal. There is a genuine legality section near the end; read it before you point this at any real volume.
What you will build
A Python script that fetches a rendered CoinGecko market page through the Crawling API, parses each coin row with BeautifulSoup, and produces one structured record per coin. The running example is the top coins on the main CoinGecko coins listing. We pull these fields:
- Name the full coin name, such as Bitcoin or Ethereum.
- Symbol the short ticker, such as BTC or ETH.
- Price the current price in your chosen fiat currency.
- Market cap the total market capitalization of the coin.
- Volume the trading volume over the last 24 hours.
- Change 24h the percentage price change over the last 24 hours.
- Change 7d the percentage price change over the last 7 days.
- Rank the market-cap rank shown on the row.
Why a plain request fails on CoinGecko
If you request a CoinGecko market page with a bare HTTP client, you often get a response with status 200 and only a fraction of the table in the body. Two things work against you. First, CoinGecko hydrates much of its market table in the browser through JavaScript: prices and percentage changes update live, and parts of the row fill in only after the page's scripts run. Parse the first response and you can capture a thin or partial table instead of the full set of rows. Second, like any high-traffic site, CoinGecko watches for automated traffic, and datacenter IPs hitting it in a tight loop get rate-limited or challenged before they reach the rendered content.
So a working scraper needs two things in one request: a browser that actually renders the page, and an IP the site reads as a real visitor. You can assemble that yourself with a headless browser plus a pool of rotating residential proxies, but stitching those together and keeping them healthy is most of the work. The Crawling API folds both into a single call: you send it the URL with a JavaScript token, it renders the page behind a trusted IP, and it returns finished HTML for you to parse.
CoinGecko offers a free public API that already returns name, symbol, price, market cap, volume, rank, and percentage changes as clean JSON. For production, use it: it is the sanctioned path and avoids parsing HTML entirely. Reach for HTML scraping only for page-only fields or views the API tier you are on does not expose, and stay within CoinGecko's stated rate limits either way.
Prerequisites
You need a few things in place before writing any code. None of them take long.
Basic Python. You should be comfortable writing and running a Python script and installing packages with pip. If you are new to the parsing side, the BeautifulSoup guide is a good companion to this tutorial.
Python 3.8 or later. Confirm your version with python --version. If you do not have it, install it from python.org or through a distribution like Anaconda, and make sure Python is on your PATH.
A Crawlbase account and JS token. Sign up, open your dashboard, and copy your JavaScript (JS) token from the account docs page. Crawlbase includes 1,000 free requests to start, which is plenty for working through this guide. Treat the token like a password: it authenticates your requests, so keep it out of version control.
Set up the project
Create a virtual environment so project dependencies stay isolated, then install the libraries the scraper needs.
python --version python -m venv coingecko_env source coingecko_env/bin/activate pip install crawlbase beautifulsoup4
On Windows, activate the environment with coingecko_env\Scripts\activate instead of the source line. Two dependencies do the work: crawlbase is the official client for the Crawling API, and beautifulsoup4 parses the returned HTML so you can pull out individual fields by CSS selector. Both json and csv ship with the standard library, so there is nothing more to install for the export step.
Step 1: Fetch a rendered CoinGecko page
Start by getting a finished page. Import the CrawlingAPI class, initialize it with your JS token, and request the CoinGecko coins URL. CoinGecko updates parts of its table client-side, so pass ajax_wait and page_wait to hold for the dynamic content before the page is captured. Checking the Crawlbase pc_status before you parse keeps failures loud instead of silent.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) OPTIONS = { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0", "ajax_wait": "true", "page_wait": 5000, } def crawl(page_url): response = api.get(page_url, OPTIONS) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['pc_status']}") return None if __name__ == "__main__": coins_url = "https://www.coingecko.com/" html = crawl(coins_url) print(html[:500] if html else "No HTML returned")
The two wait options matter for a partly client-rendered target. ajax_wait tells the API to wait for asynchronous content to finish loading, and page_wait holds for a fixed number of milliseconds after load so late-rendering cells appear before the page is captured. Five seconds is a reasonable start; raise it if rows come back thin. Run the script with python coingecko_scraper.py and you should see real CoinGecko market markup, not a stub. That confirms rendering works before you write a single selector.
CoinGecko needs a rendered page behind a trusted IP, in one call, which is exactly what the ajax_wait and page_wait options above set up. The Crawling API takes a JS token, runs the page in a real browser, rotates through residential IPs server-side, and hands you finished HTML, so you skip running a headless fleet and a proxy pool yourself. Point it at a public market page on the free tier first.
Step 2: Parse the coin rows
The CoinGecko coins page is a table where each row is one coin. Load the rendered HTML into BeautifulSoup and select the table body rows, then read each cell by its data attribute. CoinGecko tags its market cells with data-coin-table-target values, which makes them stable to target by field rather than by fragile column position.
from bs4 import BeautifulSoup def text_of(row, selector): el = row.select_one(selector) return el.get_text(strip=True) if el else None def parse_coins(html): soup = BeautifulSoup(html, "html.parser") rows = soup.select("table tbody tr") coins = [] for row in rows: name = text_of(row, '[data-coin-table-target="coinName"]') if not name: continue coins.append({ "rank": text_of(row, "td:nth-child(2)"), "name": name, "symbol": text_of(row, '[data-coin-table-target="coinSymbol"]'), "price": text_of(row, '[data-coin-table-target="price"]'), "change_24h": text_of(row, '[data-coin-table-target="priceChange24h"]'), "change_7d": text_of(row, '[data-coin-table-target="priceChange7d"]'), "volume_24h": text_of(row, '[data-coin-table-target="volume"]'), "market_cap": text_of(row, '[data-coin-table-target="marketCap"]'), }) return coins
The text_of helper queries one element inside a row and returns its stripped text, or None when the element is absent, so a coin that omits a field does not break the loop. The rank reads from the second column cell, the name and symbol come from their tagged elements, and price, the two change columns, volume, and market cap each map to a data-coin-table-target value. The if not name: continue guard skips spacer or header rows that have no coin name.
CoinGecko's class names and data-coin-table-target attributes can change without notice, and the percentage-change columns are sometimes laid out differently on narrow viewports. Treat the selectors here as a starting template, not a contract. When a field comes back None, re-inspect the live page in your browser's dev tools and update the selector. Periodic selector maintenance is normal for any production scraper, not a sign something is broken.
Step 3: Handle pagination across coin pages
One market page is a slice of the full list. CoinGecko paginates with a ?page=N query parameter, so you walk each page collecting coins up to a ceiling you set. A small retry wrapper around the fetch keeps a single slow page from ending the run.
import time def fetch_html(page_url, max_retries=2): for attempt in range(max_retries + 1): html = crawl(page_url) if html: return html if attempt < max_retries: print(f"Retrying ({attempt + 1}/{max_retries})...") time.sleep(1) print(f"Unable to fetch {page_url}") return None def collect_all_coins(base_url, max_pages): all_coins = [] for page in range(1, max_pages + 1): page_url = f"{base_url}?page={page}" html = fetch_html(page_url) if html: all_coins.extend(parse_coins(html)) time.sleep(2) return all_coins
fetch_html retries a failed fetch up to twice with a short pause, returning the HTML on success and None once it gives up. collect_all_coins walks pages one through your max_pages ceiling so a long list does not run away, parses each page into coin records, and accumulates them. The time.sleep(2) between pages paces the run so you are not hammering the site and stay friendly to its rate limits.
Step 4: Assemble the full script
Now wire the pieces into one runnable script: walk the pages, parse each coin, and export the records to both JSON and CSV.
import csv import json import time from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) OPTIONS = { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/122.0", "ajax_wait": "true", "page_wait": 5000, } def crawl(page_url): response = api.get(page_url, OPTIONS) if response["headers"]["pc_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['pc_status']}") return None def fetch_html(page_url, max_retries=2): for attempt in range(max_retries + 1): html = crawl(page_url) if html: return html if attempt < max_retries: time.sleep(1) return None def text_of(row, selector): el = row.select_one(selector) return el.get_text(strip=True) if el else None def parse_coins(html): soup = BeautifulSoup(html, "html.parser") rows = soup.select("table tbody tr") coins = [] for row in rows: name = text_of(row, '[data-coin-table-target="coinName"]') if not name: continue coins.append({ "rank": text_of(row, "td:nth-child(2)"), "name": name, "symbol": text_of(row, '[data-coin-table-target="coinSymbol"]'), "price": text_of(row, '[data-coin-table-target="price"]'), "change_24h": text_of(row, '[data-coin-table-target="priceChange24h"]'), "change_7d": text_of(row, '[data-coin-table-target="priceChange7d"]'), "volume_24h": text_of(row, '[data-coin-table-target="volume"]'), "market_cap": text_of(row, '[data-coin-table-target="marketCap"]'), }) return coins def collect_all_coins(base_url, max_pages): all_coins = [] for page in range(1, max_pages + 1): html = fetch_html(f"{base_url}?page={page}") if html: all_coins.extend(parse_coins(html)) time.sleep(2) return all_coins def save_outputs(records): with open("coingecko_coins.json", "w") as f: json.dump(records, f, indent=2) if not records: return with open("coingecko_coins.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=records[0].keys()) writer.writeheader() writer.writerows(records) def main(): coins_url = "https://www.coingecko.com/" coins = collect_all_coins(coins_url, max_pages=2) save_outputs(coins) print(f"Saved {len(coins)} coins") if __name__ == "__main__": main()
The script walks up to two market pages, fetches each with the retry wrapper, parses it into coin records, and paces the loop with a two-second sleep. save_outputs writes both a JSON file and a CSV using the keys of the first record as the header, so you have the data in whichever shape your downstream tool wants. Adjust max_pages and the base URL to fit the slice of the market you care about.
What the output looks like
Run the full script with python coingecko_scraper.py and you get a clean structured record per coin, ready for analysis, a database, or a spreadsheet. The values below are illustrative placeholders to show the shape, not live quotes.
[ { "rank": "1", "name": "Bitcoin", "symbol": "BTC", "price": "$X,XXX.XX", "change_24h": "+1.2%", "change_7d": "-3.4%", "volume_24h": "$XX,XXX,XXX,XXX", "market_cap": "$X,XXX,XXX,XXX,XXX" }, { "rank": "2", "name": "Ethereum", "symbol": "ETH", "price": "$X,XXX.XX", "change_24h": "+0.6%", "change_7d": "+2.1%", "volume_24h": "$XX,XXX,XXX,XXX", "market_cap": "$XXX,XXX,XXX,XXX" } ]
The matching CSV carries the same columns, one row per coin, which drops straight into pandas or any spreadsheet for sorting by market cap, filtering by 7 day change, or charting volume. From here the same pattern carries to other market trackers; the approach is close to scraping crypto prices from CoinMarketCap.
Staying unblocked at scale
Even with rendering handled, CoinGecko watches for scraper-shaped traffic. A few habits keep a longer run healthy, and they apply to any high-traffic data site.
- Pace your requests. Hammering pages in a tight loop is the fastest way to get throttled or challenged. The two-second sleeps above are the floor, not the ceiling; widen them for larger jobs and respect the site's published rate limits.
- Lean on rotation. A pool of residential IPs spreads requests across many real-user addresses so no single one trips a rate limit. The Crawling API handles this for you; if you roll your own stack, this is the part to get right.
-
Read the status codes. A run that starts returning non-200
pc_statusvalues is telling you the current rate or IP tier is no longer enough. Treat that as a signal to back off, not noise to ignore.
For larger crawls, the async Crawler queues requests and delivers results to a webhook, which suits running many pages without holding open connections. For the broader playbook, see how to scrape websites without getting blocked.
Is it legal to scrape CoinGecko?
Start with the obvious better path: CoinGecko publishes an official public API that returns coin name, symbol, price, market cap, 24 hour volume, percentage changes, and rank as clean JSON, with documented rate limits and paid tiers for higher throughput. For any production or commercial use, that API is the sanctioned route, and it is both more stable and more respectful of the platform than parsing rendered HTML. The HTML walkthrough in this guide is an educational fallback for page-only fields or one-off exploration, not a replacement for the API when one exists.
Whether scraping the website itself is allowed depends on CoinGecko's Terms of Service, your jurisdiction, and what you do with the data. Read CoinGecko's Terms and its robots.txt, and treat both as the boundary for what you collect and how fast you request it. Keep your request volume low enough that you are not straining its servers, and prefer the API for anything beyond light, occasional use. The market data here is factual and non-personal, which keeps it on safer ground than user-generated content, but the terms still govern automated access.
This guide is deliberately scoped to public market data: coin names, symbols, prices, market caps, volumes, percentage changes, and ranks that anyone can see without an account. It does not cover anything behind a login or paywall, any personal or account data, or the redistribution of CoinGecko's branding, logos, or editorial content, which remain its property. If your project needs reliable, high-volume access, the correct route is CoinGecko's official API or a licensed data feed, not a heavier scraper. For a wider view of providers, see the best financial data providers.
Key takeaways
- Use the API first. CoinGecko's official public API returns name, symbol, price, market cap, volume, change, and rank as JSON, and is the right path for any production workload.
-
HTML scraping is the fallback. When you do scrape the page, it is partly client-rendered, so render it with the Crawling API's JS token before you parse;
ajax_waitandpage_waitcontrol how long it waits. -
Target by data attribute. CoinGecko tags its market cells with
data-coin-table-targetvalues, so read each field by attribute rather than fragile column position. -
Paginate and export. Walk CoinGecko's
?page=Npages up to a ceiling, pace the run with short sleeps, and write the records to JSON and CSV. - Stay on public data. Respect CoinGecko's ToS, robots.txt, and rate limits, keep to public market data only, and never touch logins, accounts, or copyrighted content.
Frequently Asked Questions (FAQs)
Should I use the CoinGecko API or scrape the HTML?
Use the API. CoinGecko offers a free public API that returns coin name, symbol, price, market cap, 24 hour volume, percentage changes, and rank as clean JSON, with documented rate limits. It is more stable than parsing HTML and is the sanctioned path for production. Scrape the rendered page only for fields or views the API tier you are on does not expose, and stay within the same rate limits when you do.
Why does a plain request return only part of the CoinGecko table?
Because CoinGecko hydrates parts of its market table client-side with JavaScript: prices and percentage changes update live, and some cells fill in only after the page's scripts run in a browser. A raw HTTP request can return status 200 with a thin or partial table. To get the full set of rows you render the page first, which is what the Crawling API's JS token handles for you.
Do I need the normal token or the JS token for CoinGecko?
The JS token. The normal token fetches static HTML, which on a partly client-rendered page can miss the live cells. The JS token renders the page in a real browser before handing back the HTML, so the coin rows and their price, change, volume, and market-cap cells are present when BeautifulSoup parses them.
What data can I extract from CoinGecko?
Public market data: the coin name and symbol, the current price, market cap, 24 hour volume, the 24 hour and 7 day percentage change, and the market-cap rank. This is factual, non-personal market data visible to any visitor. Stay off anything behind a login or paywall, and do not redistribute CoinGecko's logos, branding, or editorial content.
My selectors return None. What changed?
Almost certainly CoinGecko's markup. Its class names and data-coin-table-target attributes can change without notice, and the percentage-change columns are laid out differently on narrow viewports. Re-inspect a live page in your browser's dev tools and update the selectors. Periodic selector maintenance is normal for any production scraper, which is another reason to prefer the official API where you can.
Can I use scraped CoinGecko data commercially?
Treat that as a legal question, not a technical one. CoinGecko's Terms of Service govern automated access and reuse, and commercial or high-volume use generally belongs on its official API or a paid tier rather than on a scraper. Review the terms, use the API or a licensed feed for anything at scale, and seek legal advice before building a product on top of the data.
Crawl any site at scale, without fighting infrastructure.
Crawlbase handles proxies, fingerprints, and CAPTCHAs so your team ships data pipelines instead of maintaining crawl plumbing. 1,000 requests free, no card required.
