What the OpenSea scraper collects
Scrapes opensea.io in three modes: a collection's items with listing prices, best offers, last sales, traits, rarity ranks and owners; OpenSea's own collection ranking with floors, volume and supply; and a collection's activity feed of sales, listings, offers, transfers and mints. Page 1 is read from the state OpenSea inlines into the page, later pages from the site's own GraphQL endpoint called from inside the open page. Output is JSON or CSV.
Collection items
Input: A collection slugOne row per NFT: listing price in tokens and USD, best offer, last sale, chain, token id, rarity rank, traits and owner. Cheapest first by default.
Collection ranking
Input: A ranking timeframeOne row per collection from OpenSea's own ranking, with floor price, volume and supply for the chosen window.
Activity feed
Input: A collection slug and an event typeOne row per event: sales, listings, offers, transfers or mints, priced and timestamped.
Single item
Input: An item URLOne row for one NFT with the same columns as the items mode.
From zero to OpenSea data in 3 steps
Install
Clone the repository and install one browser engine. Python 3.9+.
$ git clone https://github.com/2scraper/opensea-scraper $ cd opensea-scraper $ python3 -m venv .venv && source .venv/bin/activate $ pip install -r requirements.txt -r requirements-playwright.txt $ playwright install chromium
Run a scraper
Pick what you need and run it. Every flag is documented in the README.
$ python3 playwright_scraper.py --collection boredapeyachtclub --pages 3$ python3 playwright_scraper.py --mode collections --timeframe 1d --pages 3$ python3 playwright_scraper.py --collection pudgypenguins --mode activity --activity sales --pages 3$ python3 playwright_scraper.py --url "https://opensea.io/item/ethereum/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1"Get structured data
Rows land in JSON or CSV (
--format), with a.meta.jsonsidecar recording status, pages fetched and why the run stopped.
OpenSea data fields
34 columns per row in a predictable schema. The most useful ones:
| Field | What it holds |
|---|---|
sku | chain/contract/token_id, or chain/mint on Solana |
title | Item name |
price | Listing price as a token amount |
currency | The token the price is in, e.g. ETH |
price_usd | OpenSea's own USD conversion |
best_offer | Best offer amount |
last_sale | Last sale amount |
last_sale_currency | Token the last sale settled in; null when OpenSea does not say |
last_sale_at | Time of the last sale |
listing_marketplace | Where the listing lives (OpenSea aggregates others, e.g. blur) |
chain | Blockchain |
contract_address | Contract, or the mint on Solana |
token_id | Token id; null on Solana |
rarity_rank | Rarity rank within the collection |
traits | List of trait: value pairs |
owner_address | Owner wallet address |
[
{
"source": "opensea.io",
"scraped_at": "2026-09-17T08:22:50.208548+00:00",
"url": "https://opensea.io/item/ethereum/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1774",
"sku": "ethereum/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1774",
"title": "#1774",
"price": 6.8497666,
"currency": "ETH",
"price_usd": 16707.950690719998,
"best_offer": 6.51,
"best_offer_currency": "WETH",
"best_offer_usd": 15886.2228,
"last_sale": 6.75,
"last_sale_currency": "WETH",
"last_sale_usd": 16926.57,
"last_sale_at": "2026-09-06T21:56:59.000Z",
"listing_marketplace": "blur",
"listing_expires_at": "2026-09-17T19:37:25.000Z",
"listing_quantity": "1",
"chain": "ethereum",
"contract_address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
"token_id": "1774",
"token_standard": "ERC721",
"collection_slug": "boredapeyachtclub",
"collection_name": "Bored Ape Yacht Club",
"rarity_rank": 2412,
"traits": [
"Background: Orange",
"Clothes: Toga",
"Earring: Silver Hoop",
"… 4 more"
],
"image_url": "https://i2c.seadn.io/base/0x7e72abdf47bd21bf0ed6ea8cb8dad60579f3fb50/58d5cc3fc342034b39…",
"owner_address": "0xff53e1da7b67ae676d7742f858aab5bd4bc937f6",
"owner_username": "Check_my-bio",
"is_delisted": false,
"is_compromised": false,
"data_source": "ssr",
"page": 1,
"position": 1
},
{
"source": "opensea.io",
"scraped_at": "2026-09-17T08:22:50.208587+00:00",
"url": "https://opensea.io/item/ethereum/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1474",
"sku": "ethereum/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1474",
"title": "#1474",
"price": 6.84979,
"currency": "ETH",
"price_usd": 16708.007768,
"best_offer": 6.49,
"best_offer_currency": "WETH",
"best_offer_usd": 15837.4172,
"last_sale": 6.5,
"last_sale_currency": "ETH",
"last_sale_usd": 16276.52,
"last_sale_at": "2026-09-14T14:57:23.000Z",
"listing_marketplace": "blur",
"listing_expires_at": "2026-09-17T10:42:08.000Z",
"listing_quantity": "1",
"chain": "ethereum",
"contract_address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
"token_id": "1474",
"token_standard": "ERC721",
"collection_slug": "boredapeyachtclub",
"collection_name": "Bored Ape Yacht Club",
"rarity_rank": 7044,
"traits": [
"Background: Blue",
"Clothes: Tanktop",
"Eyes: Bloodshot",
"… 3 more"
],
"image_url": "https://i2c.seadn.io/base/0x7e72abdf47bd21bf0ed6ea8cb8dad60579f3fb50/73cfbf303c84af133f…",
"owner_address": "0xba40917080746543702c90a75d2fd36c7f333395",
"owner_username": "Tritonwarmer",
"is_delisted": false,
"is_compromised": false,
"data_source": "ssr",
"page": 1,
"position": 2
}
]First 2 of 25 rows from a real run — sample_output.json. Long values shortened here.
Handles what breaks naive OpenSea scrapers
Cursor pagination from inside the page
OpenSea publishes no address for page 2. The engines call the site's own GraphQL endpoint from the open page, with the same cookies, TLS session and exit, and parse it with the same code as page 1.
The end of the order book is detected
Under the default price sort the cursor goes null where listed items end. The run stops there as complete, and the sidecar records OpenSea's own listed and total item counts beside the rows read.
Solana items done right
On Solana the mint is the token. token_id is null and contract_address carries the mint, instead of writing an address the site does not serve on every row.
Sort is treated as the sample
--sort decides which rows you get, not just their order. The sort is recorded, and diff_runs.py refuses to compare runs taken under different sorts.
Scraping infrastructure included
The same tested core runs every 2scraper repository, so the hard parts work the same way on every site.
CAPTCHA handling
Detects the challenge a page really serves and, with a 2Captcha key, solves it only when it blocks the data — a capped number of solves per page.
Proxy rotation
Single proxy or a pool from a file, rotated on blocks with a fresh browser per exit. Credentials stay out of argv and logs.
Fingerprints
Optional complete device identity from the 2Captcha Fingerprint API — user agent, client hints, locale and timezone that agree with each other.
Scraping Browser API
Point --cdp-endpoint at a managed Chromium with its own exit country — no browser infrastructure to run yourself.
Honest run metadata
Every run writes a .meta.json sidecar and distinct exit codes: complete, blocked, empty and partial are never confused, and a failed run never overwrites good data.
Diff two runs
diff_runs.py compares two complete OpenSea snapshots by id — new, removed and changed rows for monitoring.
What teams do with OpenSea data
Floor and order-book tracking
Walk a collection cheapest-first to the end of its listed items and watch the floor move run to run.
Rarity versus price analysis
Join listing prices with rarity ranks and traits to find mispriced items.
Sales history
Collect a collection's sales feed with prices and timestamps for volume and price-trend analysis.
Collection screening
Pull OpenSea's ranking by timeframe with floors, volume and supply to compare collections.
Free to run. Pay only for what the site requires.
Runs with no key, proxy or account
No account, key or proxy: every mode returned HTTP 200 from a datacentre address, and a daily canary runs the full scrape from a GitHub runner with no secrets (measured Sep 2026). Avoid the Python-urllib user agent, which Cloudflare refuses. The paid products buy volume from many addresses, a browser you do not run (Scraping Browser API, measured identical rows), a one-request Scraper API path, and a solver for the day Cloudflare issues a managed challenge; none was met in testing.
Optional 2Captcha services — captcha solving, the Scraping Browser API, residential proxies and fingerprints — work behind one key.
Good to know
- --concurrency above 1 is refused because pagination is cursor-based.
- The Scraper API path reads one page only.
- Most of a collection has no listing price; null price is correct for unlisted items.
2scraper vs. hosted scraper APIs
| 2scraper | Typical hosted API | |
|---|---|---|
| Code | Open source, MIT — read and patch it | Closed |
| Cost | Free; optional services billed only when used | Subscription or per request / per result |
| Runs on | Your machine, server or CI | The vendor's infrastructure |
| Output | JSON/CSV files + run metadata | API responses |
| When the site changes | Fix it yourself or open an issue / PR | Wait for the vendor |
OpenSea scraper FAQ
Do I need an OpenSea account or API key?
No. The scraper reads the public pages and the site's own front-end endpoint anonymously.
Why did my run stop before the collection's total?
Under the default --sort price the walk ends at the last listed item, which is complete for the order book. Use --sort created to walk the whole collection.
Can I run pages in parallel?
No. Pages are cursor-linked, so page 5's request does not exist until page 4 is read, and --concurrency above 1 is refused. Run several collections in separate processes instead.
Are prices in USD?
price is a token amount with the token named in currency; price_usd is OpenSea's own conversion, which moves with the market.
What output formats are there?
JSON or CSV, one row class per mode, plus a .meta.json sidecar with status, stop_reason, the sort used and the collection's own totals.
Is scraping OpenSea legal?
It reads public pages at a polite rate. Follow the site's terms and the law where you operate.