Your cart is currently empty!
Category: Uncategorized
-
Scraping DAO governance and Snapshot data in 2026
Scraping DAO governance and Snapshot data in 2026
Scrape DAO governance data jobs in 2026 are a cleaner problem than most crypto scraping work because the underlying systems were designed to be public from day one. Snapshot.org publishes a full GraphQL API. Tally exposes its own GraphQL endpoint. The on-chain Governor contracts emit standardized events that any node can index. Compared to scraping a cagey marketplace or a fingerprinting-heavy social platform, governance data is right there waiting to be collected. The challenge is reconciliation across off-chain (Snapshot), on-chain (Governor contracts), and the human layer (Discord, forum threads, Twitter discussions) where most of the actual decision-making happens.
This guide covers the practical mechanics of building a DAO governance pipeline in 2026: how to use Snapshot’s GraphQL API at scale, how to read on-chain proposals from OpenZeppelin Governor and Compound-style contracts, and the analytics patterns that turn raw vote data into voter behavior intelligence.
The two-track structure of DAO governance
Almost every active DAO in 2026 runs governance on one of two tracks. Off-chain via Snapshot, where votes are gasless signatures stored on IPFS, or on-chain via a Governor contract that records every vote on the blockchain. Many DAOs do both: a Snapshot signal vote first to gauge community sentiment, then an on-chain vote that actually executes the change.
This bifurcation matters for scrapers because the data lives in completely different places. Snapshot’s data is in Snapshot’s GraphQL database. On-chain proposal data is in Ethereum, Optimism, Arbitrum, Base, or wherever the Governor contract is deployed. A complete picture of a DAO’s governance requires both feeds.
Snapshot GraphQL: the easiest scraping target in crypto
Snapshot.org runs a public GraphQL endpoint at
https://hub.snapshot.org/graphqlwith no authentication required for read operations. They rate-limit at roughly 60 requests per minute per IP, which is generous given the small payload size. The schema is documented and stable.The two queries you actually use most:
import requests SNAPSHOT_URL = "https://hub.snapshot.org/graphql" def get_proposals(space: str, first: int = 100, skip: int = 0): query = """ query GetProposals($space: String!, $first: Int!, $skip: Int!) { proposals( first: $first skip: $skip where: {space_in: [$space]} orderBy: "created" orderDirection: desc ) { id title body choices start end snapshot state author space { id name } scores scores_total votes } } """ resp = requests.post( SNAPSHOT_URL, json={"query": query, "variables": {"space": space, "first": first, "skip": skip}}, timeout=15, ) return resp.json()["data"]["proposals"] def get_votes(proposal_id: str, first: int = 1000, skip: int = 0): query = """ query GetVotes($proposal: String!, $first: Int!, $skip: Int!) { votes( first: $first skip: $skip where: {proposal: $proposal} orderBy: "vp" orderDirection: desc ) { id voter vp choice created reason } } """ resp = requests.post( SNAPSHOT_URL, json={"query": query, "variables": {"proposal": proposal_id, "first": first, "skip": skip}}, timeout=15, ) return resp.json()["data"]["votes"]The
vpfield in the votes query is voting power, denominated in whatever the space’s strategy specifies (token balance, NFT ownership, delegated tokens, etc.). For most DAOs this maps directly to token holdings at the snapshot block.A complete archive of a single DAO’s governance history requires paginating through proposals, then for each proposal paginating through votes. For a large DAO like Aave or Uniswap, this is 200-500 proposals and 50,000-200,000 individual votes. Pulling the full archive takes about an hour respecting rate limits.
Tally for on-chain governance
Tally is the dominant interface for on-chain Governor contracts. They aggregate proposal data from OpenZeppelin Governor, Compound Bravo, and several variants across multiple chains. Their GraphQL API at
https://api.tally.xyz/queryrequires an API key (free tier available, paid for higher rate limits).Tally is the right tool when you need on-chain DAO data without running your own indexer. They handle the contract decoding, proposal state machine, and cross-chain aggregation. For a research project on, say, Optimism’s governance evolution, Tally is faster than building from scratch.
def tally_proposals(governor_id: str, api_key: str, first: int = 100): query = """ query Proposals($input: ProposalsInput!) { proposals(input: $input) { nodes { ... on Proposal { id metadata { title description } status createdAt voteStats { votesCount support percent } } } } } """ resp = requests.post( "https://api.tally.xyz/query", json={ "query": query, "variables": {"input": {"filters": {"governorId": governor_id}, "page": {"limit": first}}}, }, headers={"Api-Key": api_key}, timeout=15, ) return resp.json()Reading Governor contracts directly
For the highest fidelity and complete decentralization, read the Governor contract directly via RPC. OpenZeppelin’s Governor is the most common implementation and emits these events:
ProposalCreatedwhen a new proposal is submittedVoteCastwhen a delegate casts a voteProposalCanceled,ProposalQueued,ProposalExecutedfor lifecycle changes
from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY")) GOVERNOR_ABI = [...] # OpenZeppelin Governor ABI governor = w3.eth.contract(address="0x408ED6354d4973f66138C91495F2f2FCbd8724C3", abi=GOVERNOR_ABI) def index_proposals_from_block(start_block: int, end_block: int): event_filter = governor.events.ProposalCreated.create_filter( from_block=start_block, to_block=end_block, ) for event in event_filter.get_all_entries(): yield { "proposal_id": event["args"]["proposalId"], "proposer": event["args"]["proposer"], "description": event["args"]["description"], "block_number": event["blockNumber"], "tx_hash": event["transactionHash"].hex(), }The catch with reading historical events at scale is RPC rate limits. Most providers cap
eth_getLogsat 10,000 blocks per request. For multi-year history you batch in chunks. Free tier RPC providers will throttle hard if you try to backfill years of governance events; use a paid tier or run your own archive node.Comparison of governance data sources
source auth coverage data freshness best for Snapshot GraphQL none all Snapshot spaces (off-chain votes) seconds off-chain governance, signal votes Tally GraphQL API key (free + paid) all OpenZeppelin/Compound governors minutes on-chain governance, multi-chain Direct RPC + ABI RPC key any chain you have RPC for block-by-block high-fidelity, custom contracts Etherscan API API key (free) all Ethereum contracts minutes quick contract introspection, tx decoding The Graph subgraphs varies indexed contracts only minutes when a community subgraph exists Boardroom none for read aggregated DAO data minutes quick dashboards, multi-DAO comparison For most research, Snapshot + Tally covers 80% of the data you need. Direct RPC is for the remaining 20% where you need millisecond freshness or custom contract logic that aggregators do not understand.
Snapshot space discovery
Snapshot has 100,000+ registered spaces, of which maybe 3,000 are actively used. Filtering active from inactive is a useful preprocessing step that saves storage and rate-limit budget. The discovery query:
def list_active_spaces(min_proposals: int = 10): query = """ query Spaces($first: Int!) { spaces(first: $first, orderBy: "proposalsCount", orderDirection: desc) { id name about network symbol followersCount proposalsCount } } """ resp = requests.post(SNAPSHOT_URL, json={"query": query, "variables": {"first": 1000}}, timeout=15) spaces = resp.json()["data"]["spaces"] return [s for s in spaces if s["proposalsCount"] >= min_proposals]The top 1,000 spaces by proposal count cover roughly 95% of governance activity. For a research project, scraping just those is usually enough. The long tail is interesting only for category-specific studies (e.g., NFT-collection DAOs, regional DAOs, gaming guilds).
Voter analytics: turning raw votes into intelligence
The most valuable thing you can do with governance data is voter behavior analysis. Every wallet’s voting history tells a story.
Common analytics derived from raw vote data:
- Voter participation rate: percentage of proposals a wallet voted on relative to its eligibility window
- Concentration: the share of total voting power held by the top 10, 100, 1000 voters
- Whale alignment: how often a specific large wallet votes with the majority versus against
- Coalition detection: clusters of wallets that consistently vote the same way (suggesting coordination, delegation chains, or shared sybil control)
- Proposal heat: total voters and total VP that participated, normalized by space size
A simple coalition detector using Jaccard similarity:
from collections import defaultdict from itertools import combinations def detect_coalitions(votes_by_proposal: dict, min_overlap: float = 0.8): """votes_by_proposal: {proposal_id: {voter_address: choice_index}}""" voter_history = defaultdict(dict) for prop_id, voter_choices in votes_by_proposal.items(): for voter, choice in voter_choices.items(): voter_history[voter][prop_id] = choice coalitions = [] voters = list(voter_history.keys()) for a, b in combinations(voters, 2): common = set(voter_history[a].keys()) & set(voter_history[b].keys()) if len(common) < 5: continue agreement = sum(1 for p in common if voter_history[a][p] == voter_history[b][p]) / len(common) if agreement >= min_overlap: coalitions.append((a, b, agreement, len(common))) return coalitionsThis is naive (real coalition detection uses spectral clustering or community detection algorithms on a vote agreement graph) but it works well enough for first-pass exploration on small DAOs.
Sybil and delegation graph reconstruction
A useful extension to coalition detection is reconstructing the delegation graph for token-weighted DAOs. Most Governor implementations expose a
delegates(address)view that returns the address each token-holder has delegated to, and agetVotes(address, blockNumber)view that returns effective voting power at a historical block. Walking these views for the top 10,000 token holders gives you a delegation-edge dataset that, combined with vote records, exposes:- Bridges: wallets that aggregate delegated voting power from many small holders, then vote as one bloc
- Whale puppets: delegate addresses that always vote identically with one large delegator, suggesting controlled signing
- Idle delegations: delegations to addresses that never cast votes, effectively removing those tokens from circulation
- Sybil clusters: groups of small wallets delegated to the same entity that all received their initial token transfer from a common funding source
Build the delegation graph with a network library like
networkxand run weakly-connected-component analysis to surface clusters. For DAOs with snapshot-based delegation (where delegation snapshot is per-proposal), reconstruct the graph at each proposal block to capture the actual configuration that voted, not the current one.Forum and Discord context
Governance does not happen only in the votes. The actual decision happens in forum threads, Discord channels, and Twitter discussions weeks before a proposal hits Snapshot. A complete pipeline pulls Discourse forum data via the official Discourse API, Discord channel data via bot integrations (with server admin permission), and Twitter via paid Twitter API or scraping.
For Discourse forums (Aave, Uniswap, Compound, Optimism all use Discourse for governance discussion), the API is at
https://forum.example.org/posts.jsonand is enabled by default for read access. Polling categories every 10 minutes is sufficient.def get_discourse_topics(forum_url: str, category_id: int, page: int = 0): url = f"{forum_url}/c/{category_id}.json" resp = requests.get(url, params={"page": page}, timeout=15) return resp.json()For Discord, scraping public messages without bot permissions violates Discord ToS. The compliant approach is asking the DAO’s admins for bot permissions in the relevant channels. Most governance-focused DAOs grant this for legitimate research projects.
Cross-DAO benchmarking
Once you have several DAOs in your pipeline, the most interesting analysis is cross-DAO comparison. Useful benchmarks include:
- Median quorum hit rate: what fraction of proposals reach quorum across DAOs of similar size? A DAO with a 30% quorum rate while peers run at 70% has either dead delegations or unhealthy proposal throughput.
- Proposal velocity: proposals per month relative to treasury size. A $500M treasury with one proposal per month is under-active; a $5M treasury with twenty proposals per month is firefighting.
- Author concentration: what share of proposals come from the top 5 proposal authors? Above 60% suggests a small council effectively governs and the broader voter base is rubber-stamping.
- Voter retention: of voters who participated 90 days ago, what fraction still vote today? A normal range is 25-55%; below 20% indicates community burnout.
Most of these metrics fit naturally into a single dashboard fed by the storage schema below. The benchmark numbers themselves come from your own corpus once you have indexed 30+ DAOs; there is no universally accepted source of truth for “healthy” DAO governance.
Storage schema
CREATE TABLE dao_spaces ( space_id TEXT PRIMARY KEY, name TEXT, network TEXT, members_count INTEGER, proposals_count INTEGER, treasury_usd NUMERIC ); CREATE TABLE proposals ( proposal_id TEXT PRIMARY KEY, space_id TEXT REFERENCES dao_spaces(space_id), source TEXT NOT NULL, -- 'snapshot' | 'governor' | 'tally' title TEXT, body TEXT, author TEXT, state TEXT, created_at TIMESTAMPTZ, start_at TIMESTAMPTZ, end_at TIMESTAMPTZ, snapshot_block BIGINT, scores_total NUMERIC, votes_count INTEGER ); CREATE TABLE votes ( proposal_id TEXT REFERENCES proposals(proposal_id), voter_address TEXT NOT NULL, choice INTEGER NOT NULL, voting_power NUMERIC NOT NULL, created_at TIMESTAMPTZ NOT NULL, reason TEXT, PRIMARY KEY (proposal_id, voter_address) ); CREATE INDEX ON votes (voter_address); CREATE INDEX ON proposals (space_id, created_at DESC);This schema handles both Snapshot and on-chain votes uniformly via the
sourcefield. For DAOs that run both, you have parallel proposal records and can analyze the relationship between off-chain signals and on-chain execution.Cost worked example
A practical research deployment covering ~150 active DAOs across Snapshot and 60 on-chain Governors costs:
- Snapshot GraphQL ($0)
- Tally API paid tier ($79/mo for 150 req/min)
- Alchemy Growth tier ($49/mo) for on-chain events
- 1 small VPS for indexer ($25/mo)
- Postgres on a hosted instance ($30/mo)
- 10 IPs of residential proxy for forum scraping ($20/mo)
Total: about $200/month for a complete cross-DAO governance dataset that competing services charge $1,500-3,000/month for. The break-even point is reached within the first week of operation.
Proxy considerations
Snapshot’s rate limit is permissive enough that proxies are usually unnecessary for read-only research. If you are pulling all 100,000+ Snapshot spaces continuously, distribute across 5-10 IPs with residential proxies.
For RPC calls, your bottleneck is your RPC provider, not IP-based rate limits. Use a paid tier or multiple free-tier keys. We cover provider selection in our best residential proxy providers 2026 review.
For Discord scraping (with bot permissions), the bot itself rate-limits per server, not per IP. Proxies do not help.
External authoritative reference: the Snapshot.js documentation covers the GraphQL schema, signing flow, and strategy types.
Common gotchas
- Snapshot’s
vpfield is computed at proposal creation, not at vote time. A wallet that votes after selling its tokens still gets credit for its snapshot-block balance. Do not double-discount. - The
statefield on Snapshot proposals returnspending,active,closed. There is nopassedorfailed; that is your interpretation based onscoresagainst quorum and threshold defined in the space settings. - Governor proposal IDs differ between Compound Bravo (uint256 sequential) and OpenZeppelin (keccak hash of proposal params). Code that assumes one breaks on the other.
- Multi-chain DAOs (Optimism, Arbitrum) often reuse Snapshot space slugs but have different on-chain Governors per chain. Make sure you are matching the right pair.
- Tally’s
voteStatsreturns percentages calculated against quorum, not against votes cast. A proposal at 60% support may show 30% inpercentif quorum is half the eligible supply. - Reading historical events with
eth_getLogson free-tier RPCs frequently times out on busy contracts. Chunk by 1,000-block windows for old contracts and 10,000 for newer ones, with retry-with-narrower-window on timeout.
Common analytical questions
Once you have the raw data, the questions worth answering include:
Vote concentration over time. Is a DAO’s governance becoming more or less concentrated? Plot top-10 vote share per proposal over time.
Delegate effectiveness. For DAOs with delegation (most modern Governor implementations), how often do delegates show up to vote on the tokens delegated to them? An 80% delegate participation rate is healthy; a 20% rate means delegation is dead weight.
Proposal pass rate. What fraction of proposals reach quorum and pass? A pass rate of 95% suggests rubber-stamping; a pass rate of 30% suggests genuine contention.
Treasury follow-through. For DAOs that vote on treasury allocations, do the funds actually move on-chain after the vote passes? Reconciliation against the treasury wallet’s transactions reveals whether governance is real or theater.
We dig into related on-chain forensics in our guide on scraping crypto exchange order books.
FAQ
Q: do I need a node to scrape on-chain governance?
No. Hosted RPC providers (Alchemy, Infura, QuickNode) cover the read use case perfectly. You only need your own node if you are indexing every block in real time at scale or need access to internal traces.Q: can I scrape historical Snapshot proposals from defunct DAOs?
Yes. Snapshot persists historical data even for spaces that are inactive. You can pull the full archive of any space that has not been deleted by the admins.Q: how do I correlate Snapshot space to on-chain Governor contract?
Most large DAOs publish the mapping in their docs. There is no universal index. Tally maintains a manually curated list. For obscure DAOs you may need to ask in the project’s Discord.Q: what about Solana DAOs?
Solana DAOs use Realms (governance.so) instead of Snapshot/Governor. Realms exposes data via Solana RPC and the SPL Governance program. Different stack, same patterns.Q: is voter address pseudonymity a concern for analysis?
For pure on-chain analysis, addresses are public and attribution is fair game. For combining vote data with off-chain identity (Twitter handles, Discord usernames), be careful about privacy expectations. See our GDPR compliance for web scraping guide for the data minimization patterns.Q: how do I track DAO treasury movements alongside governance votes?
IndexTransferevents from the treasury wallet on the same chain as the Governor. Cross-reference transactions occurring within 7 days after aProposalExecutedevent against the proposal’s calldata to confirm execution matched intent. Surprisingly often, a proposal authorizes a transfer that never happens because the multisig signers fail to coordinate.Q: what is the right cadence for polling Snapshot?
For active spaces, every 5 minutes catches new proposals and vote updates. For an archive, a one-time backfill plus daily incremental sync is enough. Snapshot itself does not push updates, so polling is the only option without running a custom relayer.Q: do I need to verify Snapshot signatures?
Snapshot’s hub already verifies signatures before storing votes, so the data you pull is pre-validated. Re-verifying is overkill for analysis but useful for audit-grade research where you want to prove independently that every vote is signed by the wallet it claims.Closing
DAO governance scraping in 2026 is one of the cleanest data engineering problems in crypto. Snapshot and Tally do most of the heavy lifting; on-chain data fills the gaps; forum and Discord context completes the picture. The hard part is not collection but interpretation: turning vote records into intelligence about who actually controls a DAO, how decisions get made, and whether governance is functioning or theater. For the broader crypto data infrastructure picture see our crypto-defi category hub.
-
Mistral Large for Web Scraping Pipelines in 2026
Mistral Large for Web Scraping Pipelines in 2026
Mistral Large is one of the few genuinely open-weight models that can compete with GPT-4 class systems on structured extraction — and in 2026, that matters a lot if you’re running a scraping pipeline at scale. The combination of a 128K context window, strong instruction-following, and self-hostable weights makes it worth a serious look for anyone tired of paying per-token on closed APIs.
What Mistral Large actually brings to scraping pipelines
The current release is Mistral Large 2 (mistral-large-2407), with 123B parameters. It runs comfortably on a 2xA100 or 4xA6000 setup, which puts it within reach for a dedicated scraping server. Context length is 128K tokens, enough to fit a full crawled HTML page, system prompt, and structured output schema in one shot.
For scraping-specific tasks, the key capabilities are:
- Function calling / tool use: Mistral Large supports native function calling, which means you can define a JSON schema and get reliably structured output back without regex postprocessing. The JSON mode is stable enough for production use with a schema validation layer on top.
- Instruction fidelity: On complex extraction prompts (“extract all job postings, normalize the salary field to USD, skip entries with missing location”), it follows multi-step instructions more precisely than smaller models like Mistral 7B or Mixtral 8x7B. It also handles nested schemas — arrays of objects with conditional fields — more consistently than most open models at this size.
- Multi-language support: Useful if you’re scraping non-English sites — Mistral’s training data skews toward European languages, which shows up in extraction accuracy on French, German, and Italian pages.
- Quantization tolerance: Mistral Large 2 runs well at 4-bit quantization (GPTQ or AWQ) with minimal quality degradation on extraction tasks. That gets the VRAM requirement down to around 65-70GB, which fits a 2xA100 40GB setup.
What it doesn’t bring: vision. You can’t feed it a screenshot of a rendered page the way you can with Gemini 2.0 Flash for Web Scraping, which handles multi-modal inputs natively. If your scraping targets rely on screenshots or PDFs, that’s a real gap.
Pricing comparison: Mistral Large vs alternatives
Here’s where things get interesting. Mistral via the official API is not cheap for high-volume scraping, but the self-hosted route changes the math.
Model Input (per 1M tokens) Output (per 1M tokens) Self-hostable Mistral Large 2 (API) $2.00 $6.00 Yes (weights available) GPT-4o $2.50 $10.00 No Claude 3.5 Sonnet $3.00 $15.00 No Gemini 1.5 Pro $1.25 $5.00 No Qwen 2.5 72B (API) $0.40 $0.40 Yes DeepSeek V3 $0.27 $1.10 Yes (limited) If you’re running on the Mistral API, the pricing is competitive with GPT-4o but not by a huge margin. The real advantage is owning the weights. A self-hosted Mistral Large 2 on a leased A100 box can get you below $0.10 per 1M tokens at decent throughput, which is why it competes differently from, say, a closed model.
For pure cost optimization on hosted APIs, DeepSeek V3 for cheap web scraping LLM calls is significantly cheaper. Mistral Large’s edge is openness plus quality — not raw price.
Running Mistral Large locally with vLLM
If you’re self-hosting, vLLM is the standard serving layer. Here’s a minimal setup for a scraping inference server:
# Install vLLM pip install vllm # Serve Mistral Large 2 with tensor parallelism across 2 GPUs python -m vllm.entrypoints.openai.api_server \ --model mistralai/Mistral-Large-Instruct-2407 \ --tensor-parallel-size 2 \ --max-model-len 32768 \ --dtype bfloat16 \ --port 8000Once it’s running, you call it via the OpenAI-compatible endpoint:
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused") response = client.chat.completions.create( model="mistralai/Mistral-Large-Instruct-2407", messages=[ {"role": "system", "content": "Extract structured product data as JSON."}, {"role": "user", "content": f"<html>{page_html}</html>"} ], response_format={"type": "json_object"}, temperature=0.0 )temperature=0.0is non-negotiable for extraction tasks. Any randomness and you get inconsistent field names, hallucinated prices, and outputs that break your schema validation.Integrating Mistral Large with Crawl4AI
Crawl4AI is the cleanest way to combine structured crawling with LLM extraction in 2026. It handles JS rendering, anti-bot evasion hooks, and has a native
LLMExtractionStrategythat you can point at any OpenAI-compatible endpoint — including your local Mistral Large server.from crawl4ai import AsyncWebCrawler from crawl4ai.extraction_strategy import LLMExtractionStrategy from pydantic import BaseModel class JobPosting(BaseModel): title: str company: str salary_usd: float | None location: str strategy = LLMExtractionStrategy( provider="openai/mistral-large", api_base="http://localhost:8000/v1", api_token="unused", schema=JobPosting.schema(), instruction="Extract job postings. Normalize salary to USD. Return null if missing." ) async with AsyncWebCrawler() as crawler: result = await crawler.arun( url="https://example-jobs-site.com/listings", extraction_strategy=strategy ) print(result.extracted_content)The
providerfield accepts any OpenAI-compatible base URL when combined withapi_base. No additional configuration needed. This pattern also works with Llama 3 70B local scraping pipelines or Qwen 2.5 for web scraping if you want to swap models without touching your pipeline code.Tradeoffs and when not to use it
Mistral Large is a good choice when:
- You need self-hosted weights for data privacy or compliance reasons.
- Your extraction tasks require long context (multiple pages, complex schemas).
- You’re scraping European-language sites where its training data is stronger.
- You want an open alternative to GPT-4 class quality without vendor lock-in.
It’s not the right choice when:
- You need vision/screenshot understanding — use Gemini 2.0 Flash.
- Budget is the primary constraint — Qwen 2.5 72B or DeepSeek V3 undercut it significantly on API pricing.
- You’re running a simple scraper that doesn’t need 123B parameters — Mistral 7B or Mistral Nemo handle basic extraction at a fraction of the cost.
- GPU availability is a problem — 123B quantized to 4-bit still needs ~70GB VRAM.
One thing worth flagging: Mistral’s function calling, while good, isn’t quite as rock-solid as GPT-4o on ambiguous extraction prompts. You’ll want schema validation (Pydantic works well here) and a retry loop for the ~5% of responses that don’t conform, especially on noisy HTML. A simple pattern is to catch
ValidationError, strip the HTML down to the visible text usinghtml2text, and retry once with the cleaner input — that alone drops non-conforming outputs to under 1% in most production pipelines.Latency is also worth considering. Self-hosted Mistral Large at 4-bit on 2xA100 does around 15-25 tokens/second depending on batch size and prompt length. For real-time scrapers that need sub-second responses, that’s probably too slow. For async batch extraction running overnight or across a job queue, it’s completely fine.
Bottom line
Mistral Large 2 is the strongest open-weight option for production scraping pipelines where data privacy, self-hosting, or long-context extraction matters. It’s not the cheapest route, but for teams that can’t send page content to a closed API, it’s one of the few models that actually delivers GPT-4 class extraction quality on your own infrastucture. We’ll keep benchmarking alternatives as the open-source LLM landscape moves fast — follow DRT’s AI agent scraping coverage for updated comparisons.
-
Scraping NFT collection floor prices and metadata in 2026
Scraping NFT collection floor prices and metadata in 2026
Scrape NFT data jobs in 2026 sit at the intersection of three different systems that all need to agree before you have a complete record: the marketplace API for current listings and floor price, the underlying blockchain for ownership and provenance, and IPFS or Arweave for the actual metadata and image. Pull from only one source and you have a partial picture; pull from all three and you spend most of your engineering time on rate limits, gateway timeouts, and CDN caching weirdness. The market has consolidated since the 2021-2022 peak. Three platforms (OpenSea, Blur, Magic Eden) handle the vast majority of liquidity, and the long tail of marketplaces is mostly dead. That consolidation makes the scraping problem more tractable than it was three years ago.
This guide covers the practical mechanics of building an NFT data pipeline in 2026: which marketplace APIs are public versus paid, how to reconcile on-chain truth against marketplace cache, and the rate-limit and proxy patterns that let a small operation track 10,000+ collections continuously.
What “floor price” actually means and why it is hard
The floor price of a collection is the lowest active listing price on a marketplace. It sounds simple but it has three quirks that ruin naive scrapers.
First, floor is per-marketplace. A collection might have a 0.5 ETH floor on OpenSea and a 0.45 ETH floor on Blur because of fee differences and platform-specific listings. The “true” floor is the minimum across all marketplaces where the collection trades.
Second, floor is sensitive to outliers. A single listing at a clearly broken price (1 wei, or accidentally bid in DAI instead of ETH) becomes the technical floor until it gets bought or canceled. Production trackers usually compute a “true floor” by sorting listings ascending and taking the price at the 1st percentile or after the first few listings, ignoring obvious outliers.
Third, floor changes constantly. During a popular mint or hype cycle, floor can move 5-10% in a single minute. Polling at 5-minute cadence will miss most of the movement. Real-time floor tracking requires websocket or webhook subscriptions where the marketplace offers them.
OpenSea API in 2026
OpenSea operates the most widely used NFT API. As of 2026, the v2 API requires an API key for almost everything. You can request a free key through their developer portal but the free tier is limited to 4 requests per second and excludes the highest-value endpoints (real-time order book, historical sales). Paid plans start at around $200/month for higher rate limits and at $1500/month for the data tier with full historical access.
The free tier is enough for tracking floor price across a few hundred collections at 5-minute cadence. Past that you either pay or you augment with on-chain data (which is free but has its own complexity).
import time import requests class OpenSeaClient: def __init__(self, api_key: str): self.api_key = api_key self.session = requests.Session() self.session.headers.update({ "X-API-KEY": api_key, "Accept": "application/json", }) def get_collection(self, slug: str): url = f"https://api.opensea.io/api/v2/collections/{slug}" return self._get(url) def get_listings(self, slug: str, limit: int = 50): url = f"https://api.opensea.io/api/v2/listings/collection/{slug}/all" return self._get(url, params={"limit": limit}) def get_stats(self, slug: str): url = f"https://api.opensea.io/api/v2/collections/{slug}/stats" return self._get(url) def _get(self, url, params=None, retries=3): for attempt in range(retries): resp = self.session.get(url, params=params, timeout=15) if resp.status_code == 429: time.sleep(2 ** attempt) continue resp.raise_for_status() return resp.json()The
statsendpoint is the cheapest way to track floor price because it returns the floor and total volume in a single call. Thelistingsendpoint gives more detail but costs more rate-limit budget per collection.Blur API: less documented but more powerful
Blur captured most of the professional trading volume in 2023 and has held it. Their API is technically not public but a usable endpoint exists at
https://core-api.prod.blur.io/v1/. It requires a session token that you obtain by signing a wallet message. Tracking sites like Nansen and CryptoSlam use this endpoint. Blur tolerates it as long as you stay below roughly 100 requests per minute per session.Blur’s particular value is the bid pool: a unified bidding mechanism where buyers commit ETH against an entire collection at a price. The sum of bids at each tier is a strong demand signal that does not exist on OpenSea. Scraping the Blur bid pool gives you data that is genuinely not available anywhere else without paying Blur for it directly.
def blur_collection_stats(slug: str, auth_token: str): url = f"https://core-api.prod.blur.io/v1/collections/{slug}" resp = requests.get( url, headers={ "authToken": auth_token, "User-Agent": "Mozilla/5.0", }, timeout=10, ) return resp.json()The auth token expires after about 24 hours. Production setups rotate the wallet signing automatically.
Magic Eden for Solana and multichain
Magic Eden is the dominant Solana NFT marketplace and has expanded to Bitcoin Ordinals, Polygon, and Ethereum. Their public API at
https://api-mainnet.magiceden.dev/v2/does not require an API key for most endpoints and tolerates 2 requests per second per IP.def magiceden_collection_stats(symbol: str): url = f"https://api-mainnet.magiceden.dev/v2/collections/{symbol}/stats" resp = requests.get(url, timeout=10) return resp.json()Magic Eden is the right tool when you care about Solana NFT data or Bitcoin Ordinals. For Ethereum NFTs, OpenSea and Blur have deeper liquidity and you should use them instead.
Marketplace API comparison
marketplace API auth free rate limit floor price full listing data best for OpenSea API key (free + paid) 4 req/s free yes yes (paid for full) Ethereum mainstream Blur session token ~100 req/min yes yes Ethereum trading data Magic Eden none 2 req/s yes yes Solana, Ordinals, multichain LooksRare none for public 5 req/s yes yes Ethereum royalty-aware X2Y2 API key (paid) varies yes yes Ethereum, declining Tensor none aggressive yes yes Solana professional Reservoir API key (free tier) 30k req/day free yes yes (aggregated) aggregator across all ETH chains Reservoir deserves special mention. They aggregate listings from OpenSea, Blur, LooksRare, and several others into a unified API. For most use cases, Reservoir is easier than running separate scrapers against each marketplace. The free tier is generous and covers most research work.
Marketplace decision matrix
Use this matrix when deciding which marketplace API to lean on for a given collection or use case:
use case primary fallback notes Top-100 Ethereum bluechip floor Reservoir OpenSea Reservoir’s aggregated floor catches Blur and Sudoswap that OpenSea misses Trader analytics on Ethereum Blur Reservoir Blur’s bid pool data is unique and worth the auth pain Solana NFT floor and trades Magic Eden Tensor Magic Eden has full coverage; Tensor for execution-quality data Bitcoin Ordinals Magic Eden Hiro Magic Eden Ordinals support has matured; Hiro still useful for protocol-level inscriptions L2 NFT collections (Base, Zora) Reservoir Native marketplace Reservoir has Base and Zora; smaller chains are spotty Royalty enforcement analysis LooksRare + custom Reservoir Royalty data requires per-marketplace logic that aggregators flatten Real-time floor tracking for trading bots Reservoir webhooks Marketplace websockets Webhooks remove polling latency entirely Pick the primary based on use case rather than alphabetical order. The fallback path is critical because every marketplace API has occasional outages and rate-limit surprises.
On-chain truth: when the marketplace is wrong
Marketplaces cache aggressively. A listing might be canceled or filled on-chain but still showing as active on the marketplace API for 30-60 seconds. For research this is fine. For trading or arbitrage detection it is fatal.
The authoritative source for ownership and listing status is the blockchain itself. Each marketplace operates its own listing contract (Seaport for OpenSea, BlurExchange for Blur). You can read the contract state directly via RPC and confirm which listings are still active.
from web3 import Web3 w3 = Web3(Web3.HTTPProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY")) SEAPORT_ADDRESS = "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC" def is_listing_active(order_hash: bytes) -> bool: seaport = w3.eth.contract(address=SEAPORT_ADDRESS, abi=SEAPORT_ABI) status = seaport.functions.getOrderStatus(order_hash).call() is_validated, is_cancelled, total_filled, total_size = status return is_validated and not is_cancelled and total_filled < total_sizeFor ownership, the ERC-721
ownerOf(tokenId)view is the truth. Marketplaces show owner data that may be stale by minutes. If you are scoring rarity or computing wallet holdings, you must call the contract directly.Reservoir-first architecture
A pragmatic 2026 architecture treats Reservoir as the primary read source and falls back to direct marketplace APIs only when Reservoir lacks coverage or freshness. The flow:
- Reservoir’s
tokens/v6andorders/asks/v5endpoints aggregate listings across OpenSea, Blur, LooksRare, X2Y2, Sudoswap, and a half-dozen newer venues. One request returns the cross-venue floor and the venue-by-venue breakdown. - For collections Reservoir does not cover (newer chains, niche L2s), call the native marketplace API directly.
- For sub-second freshness on top-tier collections, subscribe to Reservoir’s webhook events instead of polling.
- Cross-check Reservoir’s data once per day against Etherscan transfer counts to catch indexing lag, which appears occasionally during high-volume mint events.
The single-source pattern saves dozens of integration headaches and the multi-marketplace reconciliation work. The cost is dependency on Reservoir staying alive and pricing reasonably; have the direct-marketplace fallback paths shipped and tested even if you do not use them daily.
IPFS metadata fetching
NFT metadata for image, name, traits, and description is usually stored on IPFS or Arweave, with a token URI that resolves to a JSON document. The chain stores the URI; the URI points to the metadata; the metadata points to the image.
IPFS gateways are notoriously unreliable. The default
ipfs.iogateway frequently returns 504 timeouts. Production scrapers maintain a list of gateways and round-robin requests across them with retry logic.GATEWAYS = [ "https://ipfs.io/ipfs/", "https://cloudflare-ipfs.com/ipfs/", "https://gateway.pinata.cloud/ipfs/", "https://nftstorage.link/ipfs/", "https://w3s.link/ipfs/", ] def fetch_ipfs(cid: str, timeout: int = 10): for gateway in GATEWAYS: try: resp = requests.get(gateway + cid, timeout=timeout) if resp.status_code == 200: return resp.json() if "json" in resp.headers.get("Content-Type", "") else resp.content except requests.RequestException: continue raise IPFSFetchError(cid)For projects you scrape repeatedly, pin the metadata to your own IPFS node or upload it to S3. This eliminates gateway flakiness and makes downstream queries instant.
Proxy and rate limit strategy
Marketplace APIs are the rate-limit bottleneck for NFT scraping. The mitigation pattern is multi-key rotation rather than IP rotation. Each developer account gets its own API key, and you round-robin across keys. OpenSea allows multiple API keys per account, and you can register multiple accounts (within their terms) for additional throughput.
For unkeyed endpoints (Magic Eden, Blur with rotating tokens), proxies do help. Use residential proxies to avoid the “all your traffic from one AWS IP” pattern that gets fingerprinted. We compare options in our best residential proxy providers 2026 review.
For RPC calls to read on-chain data, you have your own rate limits with your RPC provider (Alchemy, Infura, QuickNode). Most providers have generous free tiers, and one provider key per worker process avoids cross-contamination of rate limits.
Storage schema
NFT data is multidimensional and most teams over-design the schema on day one. Start simple:
CREATE TABLE collections ( slug TEXT PRIMARY KEY, chain TEXT NOT NULL, contract_address TEXT NOT NULL, name TEXT, total_supply INTEGER, UNIQUE (chain, contract_address) ); CREATE TABLE collection_snapshots ( slug TEXT NOT NULL REFERENCES collections(slug), captured_at TIMESTAMPTZ NOT NULL, floor_price_eth NUMERIC, volume_24h_eth NUMERIC, sales_24h INTEGER, listed_count INTEGER, owner_count INTEGER, PRIMARY KEY (slug, captured_at) ); CREATE INDEX ON collection_snapshots (captured_at DESC); CREATE TABLE token_listings ( chain TEXT NOT NULL, contract_address TEXT NOT NULL, token_id NUMERIC NOT NULL, marketplace TEXT NOT NULL, price_eth NUMERIC, seller_address TEXT, listed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, order_hash TEXT, is_active BOOLEAN, PRIMARY KEY (chain, contract_address, token_id, marketplace, order_hash) );For 1000 collections at 5-minute snapshot cadence, you generate 288,000 collection snapshot rows per day plus listing-level data. PostgreSQL handles this comfortably for a year before you need to consider partitioning.
Snapshot cadence vs storage tradeoff
Cadence has compounding effects on storage and rate-limit budget. The right cadence depends on tier:
- Tier 1 (top 200 collections, mints, news-driven assets): 60 seconds. These move fast enough that a 5-minute gap loses real signal. At 200 collections this is 17,280 calls per day. With 4 OpenSea API keys you can absorb that comfortably.
- Tier 2 (next 2,000 collections): 5 minutes. Still useful for tracking trends; movement is slower so the lower cadence preserves more than 90% of meaningful signal.
- Tier 3 (long tail, 10,000+ collections): 1 hour. Catches major liquidity changes without burning the rate budget. Many long-tail collections have zero listed items most days, so most calls return identical data.
A reasonable two-week-old listing without movement can be polled daily. Reservoir’s snapshot endpoint accepts batched collection lookups, which is the cheapest way to refresh long-tail tiers in bulk.
Sales history and provenance
For provenance and sales history, the on-chain approach is more reliable than marketplace APIs. Every NFT transfer emits a
Transferevent from the ERC-721 contract. Every marketplace sale emits a marketplace-specific event (Seaport’sOrderFulfilled, etc.) that includes price.Indexing services like Goldsky, The Graph, Subsquid, and the previously mentioned Reservoir aggregate this data and expose it via GraphQL. For one-off queries, use a service. For continuous indexing of specific collections, run a node and subscribe directly to the events.
We cover the broader on-chain indexing patterns in our crypto-defi category hub and our deep dive on scraping crypto exchange order books.
External authoritative reference: the OpenSea API documentation covers the current endpoint catalog and rate-limit policy.
Cost worked example
A practical 2026 setup tracking 5,000 collections across Ethereum and Solana with mixed cadence costs roughly:
- Reservoir API free tier ($0) plus one paid key for the higher rate limits ($150/mo)
- OpenSea free key for floor stats; no paid tier needed if you reconcile via Reservoir
- Magic Eden public API ($0)
- Alchemy Growth tier for on-chain reads ($49/mo)
- Pinata or NFT.Storage for self-pinned IPFS metadata ($20/mo, 50 GB)
- $40/mo VPS for the collector (4 vCPU, 8 GB)
- $25/mo Postgres on a small managed instance
- 30 IPs of residential proxy for Blur and unauth endpoints (~$50/mo on a starter pack)
Total: about $335/month. The same coverage purchased through a vendor (NFTGo Enterprise, Nansen Query) runs $1,500-5,000/month. Self-hosting becomes a clear win once you cross 200 collections and need historical depth.
Common failure modes
The most common failure mode in NFT scrapers is treating the marketplace API as ground truth. Always reconcile against on-chain state for ownership and listing status. The second most common failure is not handling IPFS gateway timeouts. Build retry logic across multiple gateways from day one.
The third failure mode is overreacting to outlier listings. A single 0.0001 ETH listing on a 1 ETH floor collection is almost always either a wash trade attempt or a scam. Production trackers ignore listings below the 1st percentile of recent floor history.
FAQ
Q: which API gives the best Ethereum coverage?
For aggregated Ethereum data, Reservoir is the easiest path. For raw OpenSea data, the official API. For trading depth, Blur. Most production setups combine all three.Q: do I need a wallet to scrape NFT data?
Not for read-only scraping of public marketplace data. You need a wallet to access Blur’s authenticated endpoints and to interact directly with marketplace contracts on-chain. A throwaway wallet with no funds works fine for read-only authentication.Q: how do I track floor price changes in real time?
OpenSea offers a streaming events API that pushes order events. Reservoir has webhooks. Without paid endpoints, polling at 60-second cadence is the practical floor for “near real time.”Q: can I scrape rarity scores?
Rarity is computed from trait distribution, which you derive from the metadata of all tokens in a collection. Once you have the metadata table, rarity computation is trivial. Most rarity tools use the same algorithm and produce similar scores.Q: what about Bitcoin Ordinals?
Ordinals data lives on the Bitcoin chain and is indexed by services like Magic Eden, Hiro, and Ord.io. The standard Bitcoin RPC does not return Ordinal data directly; you need an indexer in front of a full node.Q: how do I tell a wash trade from a real sale?
Wash trades typically loop between two related wallets (often funded from the same source within the previous 7 days), at suspiciously round prices, with no time between transfers. Cross-reference seller and buyer addresses against a wallet-cluster service like Arkham or against your own clustering on shared funding sources. Flagging is heuristic, not exact, but rules out 80-90% of obvious wash activity.Q: do marketplaces ever sue scrapers?
Cease-and-desist letters happen, lawsuits are rare and typically reserved for projects that resell the data as a competing product. Personal use, research, and non-commercial analytics have not historically attracted enforcement. Building a public dashboard that displays floor prices crosses into commercial territory and is where you should consult counsel.Q: should I use The Graph subgraphs instead of direct RPC?
The Graph is excellent for queries that span many blocks and aggregate data, like “all sales of collection X in the last month.” Direct RPC is better for single-state lookups, like “is this listing currently active.” Use both: subgraphs for analytics, RPC for live truth.Closing
NFT scraping in 2026 is a multi-source reconciliation problem more than a pure scraping problem. The marketplace APIs give you the user-facing view; the chain gives you the truth; IPFS gives you the content. Build pipelines that treat all three as inputs and reconcile them, and you can run a research-grade NFT data system at hobbyist cost. For broader infrastructure see our crypto-defi category hub.
- Reservoir’s
-
Scraping crypto exchange order books in 2026
Scraping crypto exchange order books in 2026
Scrape crypto order books pipelines have matured into one of the most demanding data engineering problems on the public internet. A single Binance BTC/USDT depth feed pushes 50-200 updates per second during normal hours and spikes past 1000 per second during news events. Multiply that by 200 trading pairs across 8 exchanges and you have a firehose that requires careful architecture decisions before you write a single line of code. The good news is that almost every major exchange exposes order book data through public websockets that do not require API keys for read access. The bad news is that latency, message ordering, and gap recovery decisions you make in the first day of building will haunt the system for years.
This guide covers the practical mechanics of scraping crypto order books in 2026: which exchange feeds work, how to normalize depth across venues, the latency vs cost tradeoff for hosting, and the storage patterns that let small teams keep multi-month order book history without burning $20,000 a month on infrastructure.
Why scrape order books instead of buying the data
Three vendors dominate paid crypto market data: Kaiko, CryptoCompare, and Tardis.dev. They are excellent. They are also expensive: a full L2 order book history feed for 50 pairs across 5 exchanges runs $5,000-25,000 per month depending on tier. For a quant fund this is rounding error. For an indie research project, an algo trading hobbyist, or a startup building MEV tooling, it is the entire budget.
Self-collecting order book data costs roughly $200-500 per month in compute and storage if you are disciplined. The tradeoff is engineering time. The first month is hard. After that the pipeline runs itself with weekly babysitting. For research that depends on data going back further than the day you started collecting, you still need a vendor for the historical backfill, but ongoing collection is cheap.
What “order book” actually means at the wire level
Every exchange streams order book updates as either snapshots or deltas. A snapshot is the full current state of the book at a moment in time. A delta is a list of price-level changes since the last update. Most production feeds are delta-based with periodic snapshot resyncs.
A typical delta message looks like this from Binance:
{ "e": "depthUpdate", "E": 1715000000000, "s": "BTCUSDT", "U": 12345600, "u": 12345610, "b": [["63500.00", "0.5"], ["63499.50", "0"]], "a": [["63501.00", "1.2"], ["63502.00", "0.8"]] }The
bandaarrays are bid and ask updates. A quantity of0means delete that price level. TheUandufields are the first and last update IDs in this message, which you use to detect gaps. If you miss a message, you have to refetch the full snapshot from the REST endpoint and replay deltas from there.This gap recovery logic is where most amateur scrapers break. A naive listener that just appends deltas without checking sequence numbers will silently corrupt the book within minutes.
import asyncio import json import websockets import requests from collections import defaultdict class BinanceDepthListener: def __init__(self, symbol: str): self.symbol = symbol.upper() self.bids = {} # price -> qty self.asks = {} self.last_update_id = None async def fetch_snapshot(self): url = f"https://api.binance.com/api/v3/depth?symbol={self.symbol}&limit=1000" resp = requests.get(url, timeout=10) data = resp.json() self.last_update_id = data["lastUpdateId"] self.bids = {float(p): float(q) for p, q in data["bids"]} self.asks = {float(p): float(q) for p, q in data["asks"]} def apply_delta(self, msg): if msg["u"] <= self.last_update_id: return if msg["U"] > self.last_update_id + 1: raise GapDetectedError(msg["U"], self.last_update_id) for price, qty in msg["b"]: p, q = float(price), float(qty) if q == 0: self.bids.pop(p, None) else: self.bids[p] = q for price, qty in msg["a"]: p, q = float(price), float(qty) if q == 0: self.asks.pop(p, None) else: self.asks[p] = q self.last_update_id = msg["u"] async def run(self): url = f"wss://stream.binance.com:9443/ws/{self.symbol.lower()}@depth@100ms" await self.fetch_snapshot() async with websockets.connect(url) as ws: async for raw in ws: msg = json.loads(raw) try: self.apply_delta(msg) except GapDetectedError: await self.fetch_snapshot()The
@100mssuffix in the websocket URL is critical. Without it you get the default 1000ms depth stream, which is slow. The 100ms feed is the highest cadence Binance offers without paying for the institutional pro feed.Exchange feed comparison
exchange websocket URL best feed cadence snapshot via REST message ordering Binance Spot wss://stream.binance.com:9443 100ms yes, /api/v3/depth sequence IDs Binance Futures wss://fstream.binance.com 100ms yes, /fapi/v1/depth sequence IDs Coinbase wss://ws-feed.exchange.coinbase.com per event snapshot in stream sequence per product OKX wss://ws.okx.com:8443 100ms snapshot in stream checksum per update Bybit wss://stream.bybit.com/v5/public/spot per event snapshot via REST update IDs Kraken wss://ws.kraken.com/v2 per event snapshot in stream checksum per book KuCoin wss://ws-api.kucoin.com per event snapshot via REST sequence IDs Bitget wss://ws.bitget.com/v2 100ms snapshot in stream checksum per update Coinbase, OKX, and Kraken include the initial snapshot in the websocket stream when you subscribe. Binance, Bybit, and KuCoin require a separate REST call. The stream-included snapshot is faster to start with but harder to recover from on disconnect because you need to resubscribe to get a new one. The REST snapshot pattern is more flexible.
Latency budget
Order book data is only useful if you can act on it within the timeframe relevant to your strategy. For research and analytics, 100-500ms latency is fine. For market making or arbitrage, you need single-digit milliseconds. The hosting choice changes accordingly.
Binance hosts its main matching engine in AWS Tokyo. The lowest latency to its websocket is from a server in
ap-northeast-1. Coinbase runs from AWS US-East-1 (Ashburn). OKX runs from Hong Kong. Bybit from AWS Singapore. If you want sub-10ms feeds you have to host in the same AZ as the exchange and pay for cross-connect or AWS DX.For research-grade scraping, a $40/month VPS in Tokyo or Singapore from Hetzner, OVH, or Vultr gets you under 30ms to most exchanges. That is fine for everything except active trading strategies.
Normalizing depth across exchanges
Every exchange publishes its order book in slightly different formats. To do any cross-exchange analysis you need a common schema. The minimum viable normalized record:
from dataclasses import dataclass from typing import List, Tuple @dataclass class NormalizedBook: exchange: str symbol: str # canonical, like "BTC-USDT" captured_at_ms: int # exchange timestamp received_at_ms: int # local receive timestamp bids: List[Tuple[float, float]] # sorted desc asks: List[Tuple[float, float]] # sorted asc sequence: intThe two timestamps are critical.
captured_at_msis what the exchange reported.received_at_msis when your collector got the message. The difference tells you network latency. Without both you cannot diagnose feed degradation.Symbol normalization is annoying but mechanical. Binance uses
BTCUSDT, Coinbase usesBTC-USD, OKX usesBTC-USDT. Maintain a mapping table:SYMBOL_MAP = { "binance": {"BTCUSDT": "BTC-USDT", "ETHUSDT": "ETH-USDT"}, "coinbase": {"BTC-USD": "BTC-USD", "ETH-USD": "ETH-USD"}, "okx": {"BTC-USDT": "BTC-USDT", "ETH-USDT": "ETH-USDT"}, }Note that USD and USDT are not the same. Coinbase quotes against USD, most others against USDT. For arbitrage analysis you need to track this carefully and convert via the USDT/USD pair.
Storage: the real cost driver
Naive storage of every depth update across 50 pairs and 5 exchanges produces 100-500 GB per day. Compressed Parquet brings it down to 20-80 GB. Most operations cannot afford to keep raw deltas indefinitely, so the standard pattern is tiered:
- Hot: last 7 days of full deltas in ClickHouse or DuckDB
- Warm: last 90 days of 1-second OHLCV bars + L2 snapshots every minute
- Cold: indefinite history of 1-minute snapshots in compressed Parquet on S3
This gets you to roughly $50-150/month in storage for 5-exchange, 50-pair coverage. The compression ratio matters: Parquet with ZSTD level 9 hits about 8:1 on order book data because most price levels do not change between snapshots.
import pyarrow as pa import pyarrow.parquet as pq def write_snapshot_batch(snapshots: list, path: str): table = pa.Table.from_pylist(snapshots) pq.write_table( table, path, compression="zstd", compression_level=9, use_dictionary=True, )ClickHouse handles the hot path well. A modest 4-core server holds 30 days of full deltas for 50 pairs across 5 exchanges with room to query.
Recovering from a corrupted book
The deepest source of bugs is when your in-memory book diverges from reality and you do not notice for hours. Defensive checks that run continuously in the background:
- Top-of-book sanity: every minute, fetch the top of book via REST and compare to your cached book’s top bid/ask. A divergence greater than 0.5% on a liquid pair means resync.
- Crossed book detection: the highest bid should never exceed the lowest ask. If it does, the book is corrupted; trigger a full snapshot resync.
- Negative quantity detection: quantities should always be positive. Any negative value indicates a delta-application bug.
- Sequence gap counter: track the rate of detected gaps per hour. A baseline of 0.5-2 gaps per hour per pair is normal. A sudden jump indicates network degradation worth investigating before it affects downstream consumers.
- Idle channel detection: if no message arrives for a pair in 30 seconds during a normally active hour, force a reconnect. Idle does not always mean closed; the socket can hang in a half-open state.
A simple watchdog process running these checks across all pairs adds about 3% overhead and catches >95% of the silent corruption cases that otherwise show up as bad model outputs days later.
Connection management at scale
Each websocket connection costs file descriptors and memory. A single Python process with
websocketslibrary can comfortably handle 100-200 simultaneous connections. Past that, you fragment across processes or move to a more efficient runtime.For the 50-pair, 5-exchange scenario, the practical architecture is one process per exchange handling all that exchange’s pairs as a single multiplexed subscription where the protocol allows it. Binance and Coinbase both accept multi-symbol subscribe messages on a single connection. OKX requires one subscription per channel but supports multiplexing across symbols.
async def binance_multi_symbol(symbols: list[str]): streams = "/".join([f"{s.lower()}@depth@100ms" for s in symbols]) url = f"wss://stream.binance.com:9443/stream?streams={streams}" async with websockets.connect(url, ping_interval=20) as ws: async for raw in ws: msg = json.loads(raw) stream = msg["stream"] data = msg["data"] await process(stream, data)Reconnect logic must include exponential backoff plus full snapshot refetch. A 30-second disconnect during a busy hour means thousands of missed updates. Just resuming the websocket subscription will give you a corrupted book.
Proxy considerations
Public websocket endpoints generally do not enforce strict per-IP rate limits because they are designed for HFT clients with stable connections. You usually do not need a proxy for normal volume.
The exception is REST snapshot endpoints. Binance imposes a weight-based rate limit on REST: 6000 weight per minute per IP, and the depth endpoint at limit=1000 costs 50 weight. That allows about 120 snapshots per minute. If you have more than 50 pairs and a busy gap-recovery day, you can blow through the limit.
For that case, route REST snapshot traffic through a small datacenter proxy pool of 5-10 IPs. The websocket can stay on your direct connection. We cover the broader proxy strategy in our best datacenter proxy providers 2026 review.
Storage cost worked example
A practical breakdown for the standard 50-pair, 5-exchange research deployment looks like this. Raw delta volume averages 18 GB per exchange per day during normal weeks, climbing to 50-80 GB during high-volatility events. Across 5 exchanges that is 90-400 GB per day uncompressed. After ZSTD-9 Parquet compression with column dictionaries, expect 11-50 GB per day landing in cold storage. At AWS S3 Standard pricing of $0.023 per GB-month and a 90-day rolling cold tier, total storage cost falls between $25 and $110 per month for the historical archive, plus another $40-80 per month for the ClickHouse hot tier on a 4 vCPU 8 GB box.
Egress is the silent cost killer. If you ever need to move 5 TB of historical Parquet to a different cloud for a research project, AWS will bill $450 in egress alone. Either run the analytics in the same region as the bucket or use Cloudflare R2 / Backblaze B2, both of which have free or near-free egress and price storage at $0.005-0.015 per GB-month. The R2 path saves real money once the archive grows past 1 TB.
Cross-exchange arbitrage signal extraction
The classic application of order book scraping is arbitrage detection. The simplest version: for every (base asset, quote asset) pair, find the highest bid across all exchanges and the lowest ask across all exchanges. If the highest bid is greater than the lowest ask plus fees, there is an opportunity (in theory).
In practice, transfer time, withdrawal fees, exchange-specific rules, and slippage eat most of the gap. But the same data lets you compute the cross-exchange spread distribution over time, which is genuinely useful for understanding market microstructure and for backtesting more sophisticated strategies.
def best_bid_ask(books: dict[str, NormalizedBook]) -> dict: best_bid = max((b.bids[0] for b in books.values() if b.bids), key=lambda x: x[0]) best_ask = min((b.asks[0] for b in books.values() if b.asks), key=lambda x: x[0]) return { "best_bid": best_bid[0], "best_ask": best_ask[0], "spread": best_ask[0] - best_bid[0], }Common gotchas
A few traps from real production deployments:
- Binance’s
lastUpdateIdfrom the REST snapshot is occasionally lower than the first deltaUyou have already buffered. The official spec says to discard buffered deltas whereu <= lastUpdateIdand apply the rest, but ensure your buffer holds at least 200 deltas during reconnect because the snapshot can lag a busy moment by several seconds. - Coinbase’s
matchchannel and thelevel2channel are separate. To compute correct mid-price you need only level2; matches are useful for trade history but should not feed the book directly. - OKX checksums are CRC32 over a specific concatenation of the top 25 levels. If the checksum fails twice in a row, OKX expects you to resubscribe, not to reconnect the socket.
- Bybit’s v5 endpoint changed sequence semantics from v3. Old code that assumed continuous sequence numbers across all symbols on one connection will silently corrupt because v5 sequences are per symbol.
- Kraken occasionally sends a snapshot mid-stream without warning when their internal book reconciliation kicks off. If you see a
snapshotevent after subscribing, treat it as the new baseline and drop your existing book state. - Time synchronization on the collector matters. NTP drift of even 50 ms makes the
received_at_msminuscaptured_at_mslatency metric meaningless. Run chrony with multiple peers and monitor offset.
Compliance and exchange terms of service
Most exchanges’ public market data is, by their own terms of service, freely usable for personal and commercial research. Redistributing the data as a real-time feed competing with the exchange’s institutional product is a different matter and usually requires a market data license.
Binance, Coinbase, and Kraken all explicitly permit personal trading and research use of public market data. Building a competing aggregator service that resells the data is gray area. For the typical research, alpha generation, or in-house analytics use case you are fine.
External authoritative reference: the Binance API documentation covers websocket spec and rate limits.
FAQ
Q: can I scrape order books without websockets?
Yes, you can poll the REST depth endpoint. But REST polling at 1-second cadence misses 99% of updates and consumes more rate-limit budget than the websocket equivalent. Websockets are the only sensible choice for production.Q: how do I handle exchange downtime?
Most exchanges schedule maintenance windows in advance. Subscribe to their status APIs and stop reconnect attempts during announced windows to avoid wasting API budget. For unannounced outages, use exponential backoff capped at 5 minutes between retries.Q: do I need to store every delta or are snapshots enough?
Depends on use case. For backtesting you want deltas because you need event-by-event playback. For analytics on spreads and depth, 1-second snapshots are usually sufficient and 100x cheaper to store.Q: what about DEX order books?
DEXs publish state on-chain, not via websocket. You read it via RPC calls or via subgraph queries. Uniswap V3 and similar AMMs do not have order books at all; they have liquidity curves. dYdX and similar perp DEXs do have order books and expose them via gRPC. Different problem.Q: how do I detect washtrading from order book data?
Look for orders that get placed and immediately taken by the same exchange’s matching engine, identical-size orders bouncing between two price levels, and trade volume spikes that do not correspond to depth movement. This is a deep topic; treat it as a separate downstream analysis on top of the raw scraped data.Q: should I use a managed message broker like Kafka in front of the storage layer?
Only if you have multiple downstream consumers that need the live feed. For a single-consumer research pipeline, Kafka adds operational overhead without value. A simple in-process queue from listener to writer is enough. Kafka becomes worth the cost once you have a trading bot, an analytics dashboard, and a backtester all consuming the same feed concurrently.Q: do I need a colocation server for arbitrage research?
No. Colocation matters for execution, not research. A Tokyo VPS that gets order book data 30-50 ms after the matching engine is plenty for spotting historical opportunities and modeling spreads. Save the colocation budget for after you have validated the strategy.Q: can a single Python process really keep up with a busy day?
With asyncio and uvloop, yes, up to about 200 simultaneous connections and several thousand messages per second. Past that, switch to Rust, Go, or split across processes per exchange. Most research workloads never hit that ceiling.Closing
Scraping crypto order books at scale in 2026 is a tractable engineering problem if you respect the wire-level details: sequence numbers, gap recovery, snapshot resyncs, and the difference between exchange and local timestamps. The first month is the hard part. Once your collector survives a Sunday night Coinbase reconnect storm and a Binance maintenance window without corrupting state, you have a pipeline that generates millions of dollars worth of vendor data for the cost of a small VPS. For the broader market data infrastructure picture see our crypto-defi category hub.
-
Pharmaceutical Pricing Surveillance with Proxies in 2026
Pharmaceutical Pricing Surveillance with Proxies in 2026
Pharmaceutical pricing surveillance is one of the more brutal scraping problems you’ll run into in 2026. Drug prices vary by 300-800% across markets for the same molecule, reference pricing cascades across borders in real time, and every major pharmacy chain, government formulary, and parallel importer has deployed bot detection that’s gotten meaningfully harder over the past 18 months. if you’re building a pricing intelligence pipeline for pharma, you need proxies — and the wrong proxy type will burn through budget while returning garbage data.
Why pharma price data is hard to collect at scale
Pharmacy websites aren’t e-commerce. they’re hybrid: part public-facing storefront, part regulated formulary display, part insurance portal. that layering means you’re dealing with multiple anti-bot systems on the same domain. Cloudflare sits in front of the storefront. a separate WAF protects the insurance lookup. the government formulary runs on a CMS that rate-limits by IP class.
Most scrapers fail here because they treat it like a retail job. it isn’t. pricing pages often require a simulated user journey — landing page, category browse, product view — before the actual price renders. Headless browsers with datacenter IPs get flagged in under two seconds on CVS, Walgreens, Boots UK, and most EU pharmacy chains.
The core requirement is residential or mobile proxies with real ASN diversity. for country-specific pricing (which is the whole point of cross-market surveillance), you need geo-targeted IPs that actually resolve to the right country from the pharmacy’s own geolocation provider. a UK residential IP that GeoIP2 maps to the US will serve you US pricing. that’s a subtle data quality failure that’s easy to miss for weeks.
Proxy types: what actually works on pharmacy targets
Not all proxy categories perform equally across pharmaceutical sources. here’s a practical breakdown:
Proxy type Best for Avg. success rate (pharmacy sites) Cost per GB Datacenter (shared) Government formulary bulk pulls 40-60% $0.50-2 Datacenter (dedicated) Internal pricing APIs with known IP allowlists 70-85% $3-8 Residential rotating Retail pharmacy chains, insurance portals 85-95% $8-18 Mobile (4G/LTE) Heavy JS sites, TLS fingerprint-sensitive targets 90-97% $15-40 ISP static Authenticated portals, scraper-aware login flows 88-94% $10-25 Mobile proxies earn their cost premium on pharmacy targets because mobile user-agents are treated differently by most bot management vendors. DataDome and PerimeterX both apply lighter fingerprinting pressure to mobile TLS profiles. if you’re hitting Boots, Lloyds, or DocMorris, mobile IPs with rotating sessions are worth the spend.
For government sources — NHS drug tariff, FDA Orange Book, AIFA (Italy), GKV-Spitzenverband (Germany) — datacenter proxies are usually fine. these endpoints aren’t trying to sell you something. they have rate limits but rarely full bot detection stacks. the exception is NIHDI (Belgium) and some Spanish CCAA formularies that route through Cloudflare.
Building the collection pipeline
A workable pharma pricing pipeline in 2026 looks something like this:
- Segment targets by detection class (government vs. retail vs. insurance portal)
- Assign proxy tiers by segment — don’t waste mobile IPs on government PDFs
- Implement per-session fingerprint consistency: same proxy, same user-agent, same accept-language header for the full session
- Add 3-8 second randomized delays between requests on retail targets
- Parse prices with currency normalization at ingest, not post-hoc — FX drift over a multi-day crawl creates phantom price gaps
Here’s a minimal session config using Python requests + a rotating residential proxy:
import requests import random import time PROXY = "http://user-country-GB:pass@residential.provider.com:8080" headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15", "Accept-Language": "en-GB,en;q=0.9", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Referer": "https://www.google.co.uk/", } session = requests.Session() session.proxies = {"https": PROXY} session.headers.update(headers) def fetch_price_page(url): time.sleep(random.uniform(3.0, 8.0)) resp = session.get(url, timeout=20) resp.raise_for_status() return resp.textThe country code in the proxy username (
country-GB) is how most residential providers handle geo-targeting. verify that the IP you’re assigned actually resolves correctly using ipinfo.io before starting a full crawl run — provider geo accuracy varies more than the marketing suggests.Similar infrastructure patterns come up in other regulated sectors. the approach used for proxies for insurance underwriting data maps closely to pharma pricing — different industry, same problem of market-specific data sitting behind bot protection. and if you’re monitoring regulatory filings rather than retail prices, the proxies for banking compliance monitoring pattern of semi-static ISP IPs for authenticated government portals is directly applicable.
Cross-market normalization and reference pricing loops
The real analytical challenge isn’t collection. it’s normalization. pharmaceutical prices exist in at least four layers:
- Ex-factory price: what the manufacturer charges the distributor
- Wholesale price: distributor markup, varies by national regulation
- Retail pharmacy price: often fixed by formulary
- Patient out-of-pocket: after insurance rebates, co-pays, patient assistance programs
Most surveillance programs target retail because that’s what’s publicly visible. but parallel importers and tender monitors need ex-factory data, which means scraping manufacturer portals, national procurement databases, and sometimes government tender documents.
Reference pricing is where things get interesting. Germany’s GKV reimbursement benchmarks cascade into Austrian, Czech, and Slovak pricing within 6-18 months. tracking this in real time means consistent crawls across all four markets simultaneously, with timestamps accurate enough to detect which market moves first. a 24-hour crawl lag is enough to miss the signal.
For generic drug launch monitoring specifically — where the first-mover price in one market often predicts the floor price in adjacent markets — there’s a full treatment in tracking generic drug launches across global markets with proxies. that’s the resource to start with if you’re building a launch-day alert system.
Avoiding bans and managing crawl health
A few things that kill pharma pricing pipelines that wouldn’t matter on softer targets:
- TLS fingerprinting: pharmacy chains running DataDome check JA3/JA4 hashes. the requests library default TLS profile is flagged. use curl-cffi or a managed headless browser service (Browserless, Apify) for these targets.
- Cookie replay: some insurance portals invalidate sessions after 15-20 minutes. don’t cache cookies across sessions.
- Price field rendering: heavily JS-rendered price fields (CVS uses React, Walgreens uses Angular) won’t appear in raw HTML. Playwright or Puppeteer with residential proxies is the right tool, not requests.
Ban recovery matters too. if you hit a 429 or 503 on a government formulary, back off 30-60 minutes before retrying. these are often soft rate limits that reset on a fixed schedule. rotating IPs into the same blocked ASN won’t help — you need proxy rotation that also switches ASN, which most residential pools do automatically but some cheaper providers don’t.
For teams managing multiple data collection pipelines, proxies for logistics fleet tracking and public transit data covers ASN diversity management in depth, and those practices translate directly. the duplicate-record detection patterns from proxies for insurance fraud detection: public records mining are also useful when deduplicating price records across overlapping sources that cover the same product from diffrent angles.
Bottom line
For pharmaceutical pricing surveillance, residential rotating proxies are the baseline for retail targets, mobile proxies are worth the premium on fingerprint-sensitive sites, and datacenter IPs are fine for government formularies. verify geo-targeting before a full run, normalize currency at ingest, and use curl-cffi or a managed browser service on DataDome-protected targets. DRT covers proxy infrastructure across regulated industries in depth — the same patterns show up across every sector where pricing is geographically fragmented and bot protection is real.
-
Scraping YouTube comments for sentiment analysis
Scraping YouTube comments for sentiment analysis
Scrape YouTube comments and you build the foundation for one of the most expressive sentiment-analysis datasets on the public web. YouTube comments are unstructured, multilingual, opinion-rich, and reflect real reactions to media events, product launches, public figures, and content trends. The scraping landscape is shaped by three things: an official YouTube Data API that offers generous quota for most use cases, a comment system with sophisticated bot-detection that makes browser-based scraping non-trivial, and a sentiment-analysis layer that benefits dramatically from modern transformer models compared to bag-of-words approaches.
This guide covers practical patterns for building a YouTube comment-and-sentiment dataset that supports research projects from brand monitoring to political discourse analysis.
YouTube Data API as the primary path
The YouTube Data API v3 is the canonical access path for YouTube comments. The API exposes endpoints for video search, video detail, comment threads, and comment replies. Authentication uses OAuth 2.0 or API keys. The default quota is 10,000 units per day per project; comment-thread reads cost 1 unit each. For most research projects, that means roughly 10,000 comment threads per day per project.
from googleapiclient.discovery import build def get_comments(video_id: str, api_key: str, max_results: int = 100): youtube = build("youtube", "v3", developerKey=api_key) request = youtube.commentThreads().list( part="snippet,replies", videoId=video_id, maxResults=max_results, textFormat="plainText", ) all_comments = [] while request: response = request.execute() for item in response.get("items", []): top = item["snippet"]["topLevelComment"]["snippet"] all_comments.append({ "comment_id": item["id"], "author": top["authorDisplayName"], "text": top["textOriginal"], "like_count": top.get("likeCount", 0), "published_at": top["publishedAt"], "reply_count": item["snippet"].get("totalReplyCount", 0), }) request = youtube.commentThreads().list_next(request, response) return all_commentsFor research on a specific video, the API path is sufficient. For research at scale across millions of videos, the quota becomes a binding constraint. Quota expansion through Google’s standard process is realistic for legitimate research projects, but the approval process takes 4-8 weeks.
Browser-based fallback for high-volume needs
For volumes beyond the YouTube Data API quota, browser-based scraping fills the gap. YouTube’s comment system loads comments through a continuation-token-based pagination scheme that you can replicate with HTTP calls. The implementation is non-trivial because the continuation tokens are encrypted and YouTube rotates the encryption schema periodically.
The yt-dlp project (an actively maintained fork of youtube-dl) handles the comment-fetching protocol and is the practical baseline for browser-based scraping. yt-dlp’s API extracts comments via a Python interface that wraps the browser-side endpoints.
import yt_dlp def fetch_comments_ytdlp(video_url: str, max_comments: int = 1000): ydl_opts = { "skip_download": True, "writeinfojson": False, "getcomments": True, "quiet": True, "extractor_args": {"youtube": {"max_comments": [str(max_comments)]}}, } with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(video_url, download=False) return info.get("comments", [])For sustained scraping with yt-dlp, residential proxies and request-rate limiting are needed because YouTube enforces bot detection on the comment endpoints. Datacenter IPs work for short bursts but get throttled within hours.
Sentiment analysis approaches
Once you have comment text, the sentiment analysis layer is where the analytical value materializes. Three approaches with different cost and accuracy profiles:
Lexicon-based approaches (VADER, AFINN) score sentiment using word-level dictionaries. They run at millions of comments per second on a single core and require no model hosting. Accuracy is moderate (around 65-75% on labeled benchmarks) and they handle short text well. They struggle with sarcasm, negation, and code-switching.
Classical ML approaches (logistic regression on TF-IDF, fastText) produce higher accuracy (75-85%) and can be trained on domain-specific data. They are still cheap to run but require training data and ongoing model maintenance.
Transformer-based approaches (RoBERTa, DistilBERT, multilingual XLM-R) produce the highest accuracy (85-92% on English benchmarks). They are more expensive per inference but with batching can still run at thousands of comments per second on a single GPU. For most research projects, fine-tuning a small transformer on your specific domain is the right tradeoff.
from transformers import pipeline sentiment_pipeline = pipeline("sentiment-analysis", model="cardiffnlp/twitter-roberta-base-sentiment-latest") def score_sentiment(comment_text: str) -> dict: result = sentiment_pipeline(comment_text)[0] return {"label": result["label"], "score": result["score"]}For multilingual research, models like XLM-RoBERTa or the more recent multilingual sentiment models from Cardiff NLP handle 30+ languages with reasonable accuracy. For specialized domains (gaming, politics, beauty), domain-specific fine-tuning on a few thousand labeled comments produces meaningful accuracy gains.
Schema for comment-and-sentiment snapshots
CREATE TABLE youtube_comment ( comment_id VARCHAR(64) PRIMARY KEY, video_id VARCHAR(32) NOT NULL, author_channel_id VARCHAR(64), text TEXT, like_count INT, reply_count INT, published_at TIMESTAMP, sentiment_label VARCHAR(16), sentiment_score DECIMAL(5,4), language VARCHAR(8) ); CREATE INDEX comment_video_idx ON youtube_comment(video_id);For longitudinal sentiment analysis, snapshot the like_count periodically because comments accumulate engagement over time. A high-engagement comment from launch day on a music video is qualitatively different from a low-engagement comment, and the engagement count is itself a useful weighting signal in sentiment aggregations.
For broader pattern guidance, see our residential proxy provider ranking and our Python scraping libraries ranking. The Stanford NLP toolkit is also a useful reference for the underlying linguistic models.
Detecting and routing around bot challenges
When YouTube flag your traffic, the response is usually a Cloudflare or vendor interrogation page rather than a clean HTTP error. Your scraper needs to detect this content-type swap explicitly. Look for the signature
cf-mitigatedheader, the presence of__cf_chl_cookies, or HTML containingJust a moment....def is_challenged(response) -> bool: if response.status_code in (403, 503): return True if "cf-mitigated" in response.headers: return True if "__cf_chl_" in response.headers.get("set-cookie", ""): return True body = response.text[:2000].lower() return "just a moment" in body or "checking your browser" in bodyWhen you detect a challenge, do not retry on the same IP for at least 30 minutes. Mark that IP as cooling and route subsequent requests to a different IP in your pool. Aggressive retries on a flagged IP cause the cooling window to extend. For pages that absolutely must be fetched, have a fallback path that uses a headless browser. Most production setups maintain a 95/5 split between the lightweight HTTP path and the browser fallback path.
Operational monitoring and alerting
Every production scraper needs three monitoring layers. The first is per-IP success rate over a 5-minute window, alerting if any IP drops below 80%. The second is parser error rate, alerting if more than 1% of fetched pages fail to extract the canonical fields. The third is data freshness, alerting if your downstream consumers see snapshots more than 24 hours old.
import time from collections import deque class IPHealthTracker: def __init__(self, window_seconds: int = 300): self.window = window_seconds self.events = {} def record(self, ip: str, success: bool): bucket = self.events.setdefault(ip, deque()) now = time.time() bucket.append((now, success)) while bucket and bucket[0][0] < now - self.window: bucket.popleft() def success_rate(self, ip: str) -> float: bucket = self.events.get(ip) if not bucket: return 1.0 return sum(1 for _, ok in bucket if ok) / len(bucket)Wire this into Prometheus or your existing observability stack so the on-call engineer sees IP degradation as it happens rather than after the daily snapshot fails. For long-running operations, IP rotation triggered by the health tracker is more reliable than fixed rotation schedules.
Pipeline orchestration and scheduling
For any non-trivial YouTube comments scraping operation, a dedicated orchestration layer is the difference between a script you babysit and a service that runs unattended. The two strong open-source choices in 2026 are Prefect 3 and Dagster.
from prefect import flow, task @task(retries=3, retry_delay_seconds=60) def fetch_source(source_id: str, page: int): return crawl_one_page(source_id, page) @flow(name="youtube-comments-daily-sweep") def daily_sweep(source_ids: list): futures = [] for sid in source_ids: for page in range(1, 30): futures.append(fetch_source.submit(sid, page)) return [f.result() for f in futures]Run the flow on a cadence aligned to how dynamic the underlying data is. For YouTube comments where records change intraday, a 4-6 hour cadence catches meaningful movements. For longer-cycle data, daily is sufficient.
Data quality monitoring patterns
Beyond per-IP success rate, every snapshot should pass a small battery of data quality checks before being considered authoritative. Structural checks verify that every required field is present and of the expected type. Distributional checks compare the current snapshot against recent history. Semantic checks compare related fields for consistency.
def quality_check(snapshot: list[dict]) -> list[str]: errors = [] if not snapshot: errors.append("empty snapshot") return errors avg_yesterday = get_yesterday_avg_size() if len(snapshot) < avg_yesterday * 0.7: errors.append("snapshot size below threshold") return errorsRun quality checks as a separate flow that gates promotion of the snapshot from staging to production. A snapshot that fails quality checks should be quarantined for human review.
Cost optimization strategies
Proxy bandwidth is usually the dominant cost in a production scraping operation. Three optimization patterns consistently reduce cost without hurting data quality. The first is request deduplication: serve cached responses when consumers ask for the same record within the same hour. The second is conditional GET using ETag or If-Modified-Since headers when supported. The third is selective field hydration when the upstream API supports field selection.
For workloads above 100 GB of monthly proxy bandwidth, these three optimizations together reduce cost by 40-60% without changing the analytical output. The engineering effort is modest and the payback period is usually under a month at production volume.
End-to-end pipeline architecture
A production-grade scraping pipeline has four layers: collection, parsing, storage, and serving. The collection layer handles the network conversation and knows nothing about data shape. The parsing layer transforms raw bytes into structured records and owns the schema. The storage layer holds the canonical snapshots in a query-optimized format like DuckDB or ClickHouse. The serving layer exposes the data to consumers and should be denormalized and pre-aggregated where possible. Decoupling these layers also enables independent scaling.
Legal and compliance considerations
Public YouTube comments data is generally treated as fair to scrape in most jurisdictions, but always confine your collection to non-personal data. For commercial deployment, document your basis for processing, your data retention period, and your purpose limitation. The W3C Web Annotation guidance and similar published frameworks remain useful starting points for documenting your approach.
Sample analytics queries
-- Volume trend over the last 30 days SELECT date_trunc('day', snapshot_at) AS day, COUNT(*) AS records FROM snapshot WHERE snapshot_at > now() - interval '30 days' GROUP BY 1 ORDER BY 1; -- Source distribution SELECT source, COUNT(*) AS records FROM snapshot WHERE snapshot_at > now() - interval '7 days' GROUP BY source ORDER BY records DESC;Add a category share view, a source concentration view, and a price-volatility view (where applicable) and you have a solid foundation for a YouTube comments intelligence product.
Versioning your scraper for source evolution
Every YouTube comments source evolves its schema regularly. Stamp every snapshot row with the scraper version that produced it. Downstream analytics can filter by version when they need consistent semantics across a time range. Pair this with a small registry table that documents what each scraper version did differently so debugging unexpected metric jumps becomes tractable.
Caching strategy and incremental crawls
Full daily snapshots scale linearly with source size, which becomes expensive at multi-million record scale. Most production deployments shift from full snapshots to incremental refreshes after the initial ramp using freshness deadline, volatility, and business priority signals. Priority-driven scheduling reduces total request volume by 60-80% compared to blind full snapshots.
Building a brand sentiment tracker
The most common analytical product on top of YouTube comment scraping is a brand sentiment tracker that aggregates sentiment across all comments mentioning a brand across the YouTube ecosystem. The tracker requires three components: a search layer that finds comments mentioning the brand, a sentiment scoring layer, and an aggregation layer that produces daily and weekly sentiment indices.
def brand_sentiment_index(brand_keywords, comments_df): matched = comments_df[comments_df['text'].str.lower().str.contains('|'.join(brand_keywords), na=False)] return matched.groupby('snapshot_date').agg( positive_count=('sentiment_label', lambda x: (x == 'positive').sum()), negative_count=('sentiment_label', lambda x: (x == 'negative').sum()), net_sentiment=('sentiment_score', 'mean'), )For commercial brand intelligence, the headline metrics are net sentiment trend (week-over-week movement), volume trend (mention count week-over-week), and the comment-on-comment-of-comment ratio (a useful proxy for controversy intensity).
Topic modeling for emergent themes
Beyond sentiment, topic modeling reveals what people are actually talking about. Modern approaches use sentence embeddings clustered via HDBSCAN or BERTopic to surface coherent topics from comment corpora.
from sentence_transformers import SentenceTransformer from bertopic import BERTopic model = BERTopic(embedding_model=SentenceTransformer("all-MiniLM-L6-v2")) topics, probs = model.fit_transform(comments_list)For brand monitoring use cases, topic modeling reveals emergent issues that don’t yet trigger sentiment shifts. A topic cluster around “service quality” or “pricing changes” is an early indicator of brand health risk, often before the sentiment metric moves measurably.
Code-switching and multilingual comment handling
YouTube comments are heavily multilingual and frequently code-switch within a single comment (English plus Spanish, English plus Hindi, etc). Sentiment models trained on monolingual data perform poorly on code-switched text. The practical solution is to use multilingual transformer models (XLM-R, multilingual BERT) that handle the language-mixing natively. For research focused on a specific market, fine-tuning a multilingual base model on local code-switched data produces the strongest accuracy.
Author behavior and bot detection
YouTube comment sections include a meaningful fraction of bot or coordinated inauthentic activity, especially around politically sensitive content or during product launches. Filtering inauthentic comments before sentiment aggregation produces more credible analytical outputs.
The standard heuristics are: comment author with very few total comments and zero subscribers (likely bot), comment posted within seconds of video publication on a video with millions of views (likely staged), and identical comment text across multiple videos (likely coordinated). Each heuristic has false positives but combined they catch most coordinated activity.
Working with hosted scraping services
For projects where the engineering investment of running a self-hosted scraping pipeline is not justified, hosted scraping services like ScrapingBee, ZenRows, ScrapeOps, and Apify offer a different cost-and-control tradeoff. These services maintain proxy pools and headless browser fleets and expose a per-request API that abstracts away the infrastructure.
The cost model is per-request rather than per-byte. For low-volume projects (under 100,000 requests per month), the hosted services are typically cheaper than rolling your own proxy and browser infrastructure. For high-volume projects, the math flips because the per-request markup adds up at scale.
import httpx async def scrape_via_hosted(target_url: str, api_key: str): proxy_url = f"https://api.scrapingbee.com/api/v1/?api_key={api_key}&url={target_url}&render_js=true" async with httpx.AsyncClient(timeout=60) as c: r = await c.get(proxy_url) return r.textFor research projects with bounded scope, the hosted-service path is often the fastest way to ship. For ongoing production pipelines, the self-hosted path tends to win on per-request cost and on long-term flexibility.
Long-term archival and data retention
Snapshot data accumulates rapidly. A daily snapshot of even a moderate-sized dataset produces gigabytes per month and terabytes per year. The storage layer needs a clear lifecycle policy. Hot data (last 90 days) sits in your primary store for fast queries. Warm data (90 days to 2 years) sits in a cheaper columnar archive (Parquet on S3, BigQuery, ClickHouse cold storage). Cold data (older than 2 years) sits in compressed archive form, accessed rarely.
def lifecycle_archival(snapshot_age_days): if snapshot_age_days <= 90: return "hot" elif snapshot_age_days <= 730: return "warm" else: return "cold"The lifecycle policy interacts with your data retention obligations. Some jurisdictions impose maximum retention periods on certain data types. Document the retention policy in writing and audit compliance quarterly.
Common pitfalls when scraping YouTube comments
Three issues catch most teams. The first is ordering instability. YouTube’s comment thread default order is ‘Top comments’ which is engagement-weighted and changes minute by minute. Switch to ‘Newest first’ (
order=timein the Data API) for any longitudinal analysis or your snapshots will show the same comment at different positions and your ‘new comment rate’ calculation will be unreliable.The second is reply-thread truncation. The Data API returns top-level comments with up to five replies inline. Threads with more than five replies require a separate
commentThreads.listcall per parent. A scraper that ignores deep threads undercounts engagement on viral videos by 30-60%.The third is sentiment classifier drift across languages. A model fine-tuned on English YouTube comments performs 15-30% worse on Spanish, Portuguese, or Japanese comments. For multilingual datasets, use a per-language classifier or a multilingual model (XLM-R, mBERT) and validate accuracy per-language with a labeled sample before reporting aggregate sentiment.
FAQ
Is scraping YouTube comments legal?
The YouTube Data API is officially supported and its terms of service allow analytical use. Browser-based scraping outside the API violates YouTube terms of service; YouTube enforces these terms through technical countermeasures rather than legal action for non-commercial research. Confine your collection to non-personal data and document your basis for processing.How do I handle comment author personal data?
Comment authors have public display names that they chose for that comment system. The display name is technically personal data but the privacy expectation is low. Hash the author identifier in your dataset rather than storing the raw display name where possible.Can I scrape live chat from YouTube live streams?
Live chat uses a separate API surface and accumulates rapidly. Real-time analysis is possible but requires stream-processing infrastructure. For most research projects, the chat replay (available after the stream ends as a static archive) is the practical access path.What about YouTube Shorts comments?
Shorts comments use the same API surface as regular YouTube comments. The content shape is different (very short, more emoji-heavy, more reaction-style) which affects sentiment model performance. Domain-specific fine-tuning helps.How fresh is the data through the API?
The YouTube Data API reflects current state with a few-second cache lifetime. New comments appear within seconds. For real-time sentiment monitoring, polling at 1-5 minute intervals is the typical pattern.Does the YouTube Data API quota allow production sentiment monitoring?
The 10,000-unit daily quota covers roughly 10,000 comment-list calls. For brand monitoring across hundreds of videos, request a higher quota via Google’s quota-extension form.How do I detect bot or coordinated-inauthentic comment activity?
Cluster comments by author + posting cadence + text similarity. Genuine viewers post at human cadence with diverse phrasing; bot rings post in bursts with templated phrasing.To build broader social media intelligence pipelines, browse the ai-data-collection category for tooling reviews and framework deep dives.
-
The Web Scraping Playbook for E-Commerce Operators (2026)
The Web Scraping Playbook for E-Commerce Operators (2026)
If your pricing team is still doing manual spot-checks on competitor product pages, you’re already behind. this web scraping playbook for e-commerce operators covers 12 production use cases that serious operators are running in 2026 — from dynamic repricing to review mining to stockout detection. not theoretical. here’s what the stack actually looks like, and what breaks in practice.
Price intelligence and dynamic repricing
Price monitoring is the highest-ROI scraping use case in e-commerce. the math is boring but real: a 1% improvement on $10M GMV is $100K. most teams start here and never stop.
the standard stack is Playwright for JavaScript-heavy pages, Scrapy for bulk catalogue crawls, and a rotating residential or mobile proxy pool to avoid blocks. for Amazon and major retailers, undetected-chromedriver paired with a rotating residential mobile proxy layer is still the most reliable combo in 2026, especially for geo-specific pricing — Singapore vs. US prices on electronics can differ by 20% or more.
a minimal repricing loop looks like this:
import httpx, json PROXY = "http://user:pass@residential-pool.example.com:8080" HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} def fetch_price(url: str) -> float: r = httpx.get(url, headers=HEADERS, proxies={"https://": PROXY}, timeout=15) data = json.loads(r.text) # assumes JSON product API return float(data["price"])run this every 15 to 30 minutes per ASIN or SKU, store deltas in Postgres, and alert when a competitor drops more than 5% on a top-100 product. you’d be surprised how many mid-sized operators still don’t have this wired up.
Geo-pricing arbitrage detection
retailers like Nike and Samsung serve different prices by country. scrape from multiple IP exit nodes (SG, US, UK, DE) on the same product URL and log the spread. operators selling cross-border use this to undercut on the right market. it’s one of the cleaner arbitrage plays available without a huge data budget.
Catalogue and assortment intelligence
knowing what competitors stock is as valuable as knowing what they charge. sometimes more. use cases here include:
- new SKU detection: scrape category pages nightly, diff against yesterday’s snapshot, alert on new listings
- stockout monitoring: flag “out of stock” labels, cross-reference with your own inventory to capture demand
- assortment gap analysis: which subcategories do they carry that you don’t?
- bundle and kit tracking: competitors hide margin in bundles, and scraping product page structure reveals the strategy
for large catalogues (100K+ SKUs), Scrapy with AutoThrottle and a rotating datacenter proxy pool is the pragmatic choice. residential proxies are overkill for pure catalogue crawls where bot detection is light. save the mobile IPs for checkout-flow and pricing pages that trigger heavy fingerprinting.
SaaS operators run the same intelligence loops on vendor and competitor product catalogues — the scraping patterns carry over directly if you’re building something reusable across verticals.
Review and sentiment mining
1-star reviews on a competitor’s bestseller are a free focus group. scrape Amazon, Trustpilot, Google Shopping, and platform-native reviews on a weekly cadence. clean the text, run it through Claude Haiku or GPT-4o-mini for classification, and bucket by complaint theme.
common findings that actually move product decisions:
- packaging complaints (fragile, poor unboxing) — opening for premium positioning
- sizing inconsistency — an angle if you publish detailed fit specs
- slow shipping — real leverage if you hold local inventory
- missing accessories — bundle opportunity hiding in plain sight
for Amazon specifically, use a residential rotating proxy and randomise request cadence between 3 and 8 seconds. Amazon’s bot detection is session-aware. a clean residential IP with a consistent session fingerprint outperforms rapid-cycling datacenter IPs by a wide margin. learned that one the hard way.
marketing agencies running brand audits use identical review pipelines to benchmark client sentiment against competitors — worth reading if you need to present findings to non-technical stakeholders, not just engineers.
Ad creative and keyword intelligence
scraping paid ad creatives and organic keyword data gives you a real-time view into competitor messaging. the most useful sources:
source what you get best tool block risk Google Shopping SERP ad copy, price, seller DataForSEO / SerpAPI low (API) Meta Ad Library creatives, run duration, CTA Playwright + residential medium Amazon search suggest long-tail keyword demand httpx + datacenter proxy low TikTok Shop trending viral SKU signals Playwright + mobile proxy high G2 / Trustpilot category share-of-voice Scrapy low TikTok Shop deserves its own pipeline in 2026. scrape trending product pages, cross-reference with your catalogue, and use viral velocity (view count acceleration over 48h) as a leading demand signal. mobile proxies with SG exit nodes are necessary here. TikTok fingerprints browser and IP type combinations aggressively, and datacenter IPs die fast.
alt-data investors run similar SERP and ad-intelligence pipelines to track brand spend as a proxy for growth — interesting to see how the same data gets read on the financial side.
Influencer and affiliate sourcing at scale
finding micro-influencers who already talk about your product category is a legitimate scraping use case. the pipeline: scrape hashtag pages on Instagram and TikTok, extract @handles and follower counts, filter by engagement rate (likes+comments / followers > 3%), then enrich with email lookup via Hunter or Apollo.
this is where the legal and ToS line gets real. scraping public post metadata (counts, captions, hashtags) from public profiles sits in a defensible grey zone. scraping DMs or private account data doesn’t. know the diffrrence before you build.
recruitment agencies use structurally identical candidate-sourcing scrapers to find passive talent on LinkedIn and GitHub — the extraction and enrichment patterns are reusable across both cases.
for the proxy layer, mobile residential IPs with per-request rotation are standard. Instagram and TikTok fingerprint at the TLS and HTTP/2 level. a static datacenter IP lasts maybe 20 requests before a CAPTCHA wall. not usable at scale.
Avoiding detection at scale
four levers that actually matter:
- proxy type: mobile residential beats static residential beats datacenter for social and retail targets
- TLS fingerprint: use curl-impersonate or Playwright with a real Chrome profile, not raw httpx
- request cadence: randomise delay between 2 and 12 seconds, simulate scroll events on JS-heavy pages
- session management: warm sessions with a few organic-looking actions before hitting target data
Bottom line
operators with price intelligence, review mining, ad creative tracking, catalogue diffing, and influencer sourcing in production have a measurable data edge over teams that don’t. start with one use case, get it to production reliability, then add the next. dataresearchtools.com covers each of these scraping verticals in depth, with stack-specific guides and proxy comparisons updated for 2026 realities.
-
Scraping podcast metadata at scale in 2026
Scraping podcast metadata at scale in 2026
Scrape podcast metadata and you build the foundation for one of the most analytically rich audio datasets. The global podcasting ecosystem has crossed 5 million shows distributed across Apple Podcasts, Spotify, Amazon Music, YouTube, and the long tail of independent podcast hosting platforms. Each show has a structured RSS feed with episode metadata, but the discovery layer (what is the show titled, who is the host, what categories does it sit in, how popular is it) sits across multiple aggregators with different schemas. The scraping landscape is shaped by three things: a healthy open-RSS foundation that makes episode-level scraping straightforward for any show with a public RSS feed, an Apple Podcasts directory that remains the canonical discovery source, and Spotify’s growing portfolio of exclusive shows that sit outside the open RSS ecosystem.
This guide covers the practical patterns for building a podcast metadata dataset spanning the major platforms. The patterns work for both academic research projects and commercial intelligence products.
Source taxonomy and identifiers
The podcast ecosystem has three distinct source types with different access patterns.
RSS feeds are the open foundation. Every podcast that wants distribution publishes an RSS feed conforming to the iTunes Podcast Spec or the Podcasting 2.0 Spec. The feed includes show metadata (title, description, host, categories, image) and per-episode metadata (title, description, publish date, duration, audio URL, transcript URL if Podcasting 2.0). RSS feeds are public and scrape-friendly by design.
Aggregator directories (Apple Podcasts, Spotify, Podchaser, Listen Notes) consolidate metadata across millions of shows into searchable interfaces. Apple Podcasts publishes its catalogue through the iTunes Search API and an undocumented chart API. Spotify publishes through the official Spotify Web API. Podchaser and Listen Notes both expose paid commercial APIs.
Hosting platforms (Megaphone, Anchor, Libsyn, Buzzsprout, Transistor) host the audio files and generate the RSS feeds. Some expose platform-level analytics that aren’t in the public RSS, but most podcast intelligence relies on the public RSS plus aggregator data rather than per-host scraping.
import feedparser import httpx async def fetch_rss_feed(feed_url: str, proxy: str = None): async with httpx.AsyncClient(proxy=proxy, timeout=30) as c: r = await c.get(feed_url) if r.status_code == 200: feed = feedparser.parse(r.text) return { "title": feed.feed.get("title"), "description": feed.feed.get("description"), "language": feed.feed.get("language"), "categories": [t.get("term") for t in feed.feed.get("tags", [])], "episodes": [ { "title": e.get("title"), "published": e.get("published"), "duration": e.get("itunes_duration"), "audio_url": next((l.get("href") for l in e.get("links", []) if l.get("type", "").startswith("audio/")), None), } for e in feed.entries ], } return NoneFor an enterprise-grade podcast metadata pipeline, build a registry of canonical RSS feed URLs (sourced from Apple Podcasts plus Listen Notes), refresh each feed on a cadence aligned to publish frequency, and store every episode as a snapshot row.
Apple Podcasts directory access
Apple Podcasts publishes the canonical podcast directory through two main channels. The iTunes Search API at
https://itunes.apple.com/searchaccepts a free-text query and returns matching podcasts with collectionId, feedUrl, and country code. The undocumented charts API returns per-genre top-charts that update daily.async def search_itunes(term: str, country: str = "US"): url = "https://itunes.apple.com/search" params = { "term": term, "media": "podcast", "country": country, "limit": 200, } async with httpx.AsyncClient(timeout=20) as c: r = await c.get(url, params=params) if r.status_code == 200: return r.json().get("results", []) return []The iTunes Search API has generous rate limits for non-commercial use and is a solid foundation for show discovery. For commercial use at scale, Listen Notes and Podchaser sell more comprehensive databases that include shows that don’t appear in iTunes.
Spotify-specific considerations
Spotify hosts a growing portfolio of exclusive shows (Joe Rogan, Ringer Network, Gimlet Media catalog) that don’t have public RSS feeds. For these shows, the Spotify Web API is the canonical access path. The API requires OAuth authentication but the rate limits are generous for the show-search and show-detail endpoints.
import httpx async def search_spotify(term: str, access_token: str): url = "https://api.spotify.com/v1/search" params = {"q": term, "type": "show", "limit": 50} headers = {"Authorization": f"Bearer {access_token}"} async with httpx.AsyncClient(timeout=20) as c: r = await c.get(url, params=params, headers=headers) if r.status_code == 200: return r.json().get("shows", {}).get("items", []) return []For comprehensive coverage of the Spotify exclusive catalogue, plan for ongoing OAuth token rotation because Spotify’s tokens expire on 1-hour intervals. A token-refresh service that maintains a pool of valid tokens is part of any production Spotify integration.
Episode-level engagement signal
Episode metadata is publicly available, but episode engagement (downloads, listens, completion rate) is mostly behind hosting platform analytics. For external research, the closest available signals are listener review and rating activity on Apple Podcasts and Spotify, plus episode-level chart positions where they exist.
CREATE TABLE podcast_episode_snapshot ( snapshot_at TIMESTAMP NOT NULL, show_id VARCHAR(64) NOT NULL, episode_id VARCHAR(128) NOT NULL, title TEXT, published_at TIMESTAMP, duration_seconds INT, audio_url TEXT, transcript_url TEXT, PRIMARY KEY (snapshot_at, show_id, episode_id) );For research that needs engagement signals, the Apple Podcasts review scraping pattern is the most practical. Reviews accumulate over the life of a show and the review velocity per episode is itself a useful proxy for engagement intensity.
For broader pattern guidance, see our residential proxy provider ranking and our Python scraping libraries ranking.
Detecting and routing around bot challenges
When podcast directories and platforms flag your traffic, the response is usually a Cloudflare or vendor interrogation page rather than a clean HTTP error. Your scraper needs to detect this content-type swap explicitly. Look for the signature
cf-mitigatedheader, the presence of__cf_chl_cookies, or HTML containingJust a moment....def is_challenged(response) -> bool: if response.status_code in (403, 503): return True if "cf-mitigated" in response.headers: return True if "__cf_chl_" in response.headers.get("set-cookie", ""): return True body = response.text[:2000].lower() return "just a moment" in body or "checking your browser" in bodyWhen you detect a challenge, do not retry on the same IP for at least 30 minutes. Mark that IP as cooling and route subsequent requests to a different IP in your pool. Aggressive retries on a flagged IP cause the cooling window to extend. For pages that absolutely must be fetched, have a fallback path that uses a headless browser. Most production setups maintain a 95/5 split between the lightweight HTTP path and the browser fallback path.
Operational monitoring and alerting
Every production scraper needs three monitoring layers. The first is per-IP success rate over a 5-minute window, alerting if any IP drops below 80%. The second is parser error rate, alerting if more than 1% of fetched pages fail to extract the canonical fields. The third is data freshness, alerting if your downstream consumers see snapshots more than 24 hours old.
import time from collections import deque class IPHealthTracker: def __init__(self, window_seconds: int = 300): self.window = window_seconds self.events = {} def record(self, ip: str, success: bool): bucket = self.events.setdefault(ip, deque()) now = time.time() bucket.append((now, success)) while bucket and bucket[0][0] < now - self.window: bucket.popleft() def success_rate(self, ip: str) -> float: bucket = self.events.get(ip) if not bucket: return 1.0 return sum(1 for _, ok in bucket if ok) / len(bucket)Wire this into Prometheus or your existing observability stack so the on-call engineer sees IP degradation as it happens rather than after the daily snapshot fails. For long-running operations, IP rotation triggered by the health tracker is more reliable than fixed rotation schedules.
Pipeline orchestration and scheduling
For any non-trivial podcast metadata scraping operation, a dedicated orchestration layer is the difference between a script you babysit and a service that runs unattended. The two strong open-source choices in 2026 are Prefect 3 and Dagster.
from prefect import flow, task @task(retries=3, retry_delay_seconds=60) def fetch_source(source_id: str, page: int): return crawl_one_page(source_id, page) @flow(name="podcast-metadata-daily-sweep") def daily_sweep(source_ids: list): futures = [] for sid in source_ids: for page in range(1, 30): futures.append(fetch_source.submit(sid, page)) return [f.result() for f in futures]Run the flow on a cadence aligned to how dynamic the underlying data is. For podcast metadata where records change intraday, a 4-6 hour cadence catches meaningful movements. For longer-cycle data, daily is sufficient.
Data quality monitoring patterns
Beyond per-IP success rate, every snapshot should pass a small battery of data quality checks before being considered authoritative. Structural checks verify that every required field is present and of the expected type. Distributional checks compare the current snapshot against recent history. Semantic checks compare related fields for consistency.
def quality_check(snapshot: list[dict]) -> list[str]: errors = [] if not snapshot: errors.append("empty snapshot") return errors avg_yesterday = get_yesterday_avg_size() if len(snapshot) < avg_yesterday * 0.7: errors.append("snapshot size below threshold") return errorsRun quality checks as a separate flow that gates promotion of the snapshot from staging to production. A snapshot that fails quality checks should be quarantined for human review.
Cost optimization strategies
Proxy bandwidth is usually the dominant cost in a production scraping operation. Three optimization patterns consistently reduce cost without hurting data quality. The first is request deduplication: serve cached responses when consumers ask for the same record within the same hour. The second is conditional GET using ETag or If-Modified-Since headers when supported. The third is selective field hydration when the upstream API supports field selection.
For workloads above 100 GB of monthly proxy bandwidth, these three optimizations together reduce cost by 40-60% without changing the analytical output. The engineering effort is modest and the payback period is usually under a month at production volume.
End-to-end pipeline architecture
A production-grade scraping pipeline has four layers: collection, parsing, storage, and serving. The collection layer handles the network conversation and knows nothing about data shape. The parsing layer transforms raw bytes into structured records and owns the schema. The storage layer holds the canonical snapshots in a query-optimized format like DuckDB or ClickHouse. The serving layer exposes the data to consumers and should be denormalized and pre-aggregated where possible. Decoupling these layers also enables independent scaling.
Legal and compliance considerations
Public podcast metadata data is generally treated as fair to scrape in most jurisdictions, but always confine your collection to non-personal data. For commercial deployment, document your basis for processing, your data retention period, and your purpose limitation. The W3C Web Annotation guidance and similar published frameworks remain useful starting points for documenting your approach.
Sample analytics queries
-- Volume trend over the last 30 days SELECT date_trunc('day', snapshot_at) AS day, COUNT(*) AS records FROM snapshot WHERE snapshot_at > now() - interval '30 days' GROUP BY 1 ORDER BY 1; -- Source distribution SELECT source, COUNT(*) AS records FROM snapshot WHERE snapshot_at > now() - interval '7 days' GROUP BY source ORDER BY records DESC;Add a category share view, a source concentration view, and a price-volatility view (where applicable) and you have a solid foundation for a podcast metadata intelligence product.
Versioning your scraper for source evolution
Every podcast metadata source evolves its schema regularly. Stamp every snapshot row with the scraper version that produced it. Downstream analytics can filter by version when they need consistent semantics across a time range. Pair this with a small registry table that documents what each scraper version did differently so debugging unexpected metric jumps becomes tractable.
Caching strategy and incremental crawls
Full daily snapshots scale linearly with source size, which becomes expensive at multi-million record scale. Most production deployments shift from full snapshots to incremental refreshes after the initial ramp using freshness deadline, volatility, and business priority signals. Priority-driven scheduling reduces total request volume by 60-80% compared to blind full snapshots.
Building a podcast trends dashboard
The most common analytical product on top of podcast metadata scraping is a trends dashboard that tracks new shows launching per category per week, episode publish frequency per show, and chart-position movements over time. The dashboard layer aggregates the raw snapshot data into rolled-up views suitable for fast queries.
def category_trends(df): return df.groupby(['primary_category', 'snapshot_week']).agg( new_shows=('show_id', lambda x: (~x.isin(prev_week_shows)).sum()), active_shows=('show_id', 'nunique'), median_episodes=('episode_count', 'median'), )For commercial podcast intelligence, the headline metrics are category market share by show count, by total downloads where available, and by chart presence. Each view answers a different question about category dynamics.
For research focused on individual shows, the most useful derived metric is publish-cadence stability: how consistently a show publishes on its stated schedule. Shows that drift from weekly to bi-weekly to sporadic are usually losing momentum; shows that increase cadence (weekly to twice-weekly) are usually growing.
Episode transcript analytics
When transcripts are available (through Podcasting 2.0 transcript elements or paid services), the analytical surface expands dramatically. Topic modeling, named entity extraction, brand mention tracking, and quote-level search all become possible. For brand monitoring use cases, podcast transcript analytics fills the gap between text-based social listening and broadcast monitoring.
The economics of transcribing on demand vary. AssemblyAI runs at roughly $0.0003-$0.0006 per second of audio. For a 60-minute episode, that’s $1-2 per transcript. For a research project covering 1,000 priority shows with weekly episodes, the annual transcript cost is $50-100k, which is feasible for most institutional research budgets.
Cross-platform show identity
The same podcast appears on multiple platforms with platform-specific identifiers. Apple Podcasts uses collectionId, Spotify uses a Spotify show ID, Listen Notes uses its own ID. Cross-platform identity resolution uses the canonical RSS feed URL where available, falling back to title plus host plus first-episode-date for shows where RSS isn’t published. A maintained cross-walk table is part of any production podcast intelligence dataset.
Working with hosted scraping services
For projects where the engineering investment of running a self-hosted scraping pipeline is not justified, hosted scraping services like ScrapingBee, ZenRows, ScrapeOps, and Apify offer a different cost-and-control tradeoff. These services maintain proxy pools and headless browser fleets and expose a per-request API that abstracts away the infrastructure.
The cost model is per-request rather than per-byte. For low-volume projects (under 100,000 requests per month), the hosted services are typically cheaper than rolling your own proxy and browser infrastructure. For high-volume projects, the math flips because the per-request markup adds up at scale.
import httpx async def scrape_via_hosted(target_url: str, api_key: str): proxy_url = f"https://api.scrapingbee.com/api/v1/?api_key={api_key}&url={target_url}&render_js=true" async with httpx.AsyncClient(timeout=60) as c: r = await c.get(proxy_url) return r.textFor research projects with bounded scope, the hosted-service path is often the fastest way to ship. For ongoing production pipelines, the self-hosted path tends to win on per-request cost and on long-term flexibility.
Long-term archival and data retention
Snapshot data accumulates rapidly. A daily snapshot of even a moderate-sized dataset produces gigabytes per month and terabytes per year. The storage layer needs a clear lifecycle policy. Hot data (last 90 days) sits in your primary store for fast queries. Warm data (90 days to 2 years) sits in a cheaper columnar archive (Parquet on S3, BigQuery, ClickHouse cold storage). Cold data (older than 2 years) sits in compressed archive form, accessed rarely.
def lifecycle_archival(snapshot_age_days): if snapshot_age_days <= 90: return "hot" elif snapshot_age_days <= 730: return "warm" else: return "cold"The lifecycle policy interacts with your data retention obligations. Some jurisdictions impose maximum retention periods on certain data types. Document the retention policy in writing and audit compliance quarterly.
Common pitfalls when scraping podcast metadata
Three issues recur across podcast-data projects. The first is feed-vs-platform drift. The RSS feed is the canonical source for episode metadata, but Apple Podcasts, Spotify, and YouTube Music apply their own normalization. Episode titles, descriptions, and even durations can differ between the feed and the platform listing. For analytical work, treat the RSS feed as authoritative and treat platform fields as observations.
The second is dynamic-ad-insertion duration jitter. Podcasts that use DAI (Megaphone, Acast, Spreaker) can ship the same episode at slightly different durations to different listeners depending on the ad load. A scraper that pulls duration from the platform instead of the source RSS sees apparent variance that is purely insertion artifact.
The third is enclosure-URL impermanence. The audio URL in the RSS feed often points to a tracking-prefix domain (e.g.,
chrt.fm/track/...) that 302-redirects to the actual CDN asset. Following the redirect inflates the publisher’s download counter, which is ethically gray for research scraping. Read the URL but do not follow it unless your research design requires the audio.FAQ
Is scraping podcast RSS feeds legal?
RSS feeds are public by design. Podcasts that don’t want public consumption don’t publish RSS. Scraping public RSS is generally fair. The audio files themselves are subject to copyright; treat the metadata as fair to scrape and the audio as licensed content that requires permission to redistribute.Can I get download numbers for podcasts?
Most download numbers are private and held by the hosting platform plus the podcaster. Podtrac publishes monthly download rankings for shows that opt in, and Podchaser publishes ranked positions. For research that needs download counts, these published-by-publisher numbers are the practical source.How do I handle podcasts in non-English languages?
The patterns transfer directly. Apple Podcasts has country-specific charts for 150+ countries. RSS feeds are encoded in UTF-8 by spec. The main challenge is text processing for non-Latin scripts where standard NLP tools may need locale-specific configuration.What about transcripts?
Podcasting 2.0 includes a transcript element that links to a structured transcript file (usually JSON or VTT). Adoption is growing but still under 30% of major shows. For shows without published transcripts, services like AssemblyAI or Deepgram can transcribe audio at $0.00025-$0.001 per minute, which is feasible for analytical projects on selected shows.Can I track guest appearances across shows?
Guest appearances are typically not structured in RSS. Some podcasts mention guests in the episode description, which can be parsed with NLP. Podchaser maintains a curated guest database that may be more practical for research projects focused on cross-show guest networks.Where do I find a comprehensive podcast directory in 2026?
Podcast Index and Listen Notes both publish open-ish APIs covering 4M+ podcasts. The Apple Podcasts directory is still the largest discovery surface but exposes only paginated search rather than a bulk download.How do I detect when an episode is removed or made private?
Diff the RSS feed daily and emit a row whenever a previously-seen GUID disappears. Some publishers rotate to GUID-less feeds; in that case fall back to (title + pubDate) as the stable key.To build broader audio intelligence pipelines, browse the dev-tools-projects category for tooling reviews and framework deep dives.
-
How to Scrape Google Play Store Reviews and Install Counts (2026)
How to Scrape Google Play Store Reviews and Install Counts (2026)
Google Play Store has over 3.5 million apps, and for competitive intelligence, market research, or app analytics, scraping Play Store reviews and install counts is one of the most valuable data collection tasks you can run. The problem is that Google actively blocks scraping — rate limits, CAPTCHAs, and a shifting API surface make naive approaches fail within minutes. this guide covers what actually works in 2026: the unofficial internal API, third-party libraries, and browser automation fallbacks.
What data you can actually get
Install counts on Google Play are reported in ranges (“1M+” or “500K+”), not exact numbers. that’s annoying but workable for most use cases. reviews, on the other hand, are rich: star rating, text, reviewer name, date, device type, and app version. here’s what’s available per app listing:
Field Source Exact or Range Install count HTML / internal API Range only (e.g., “10M+”) Rating score HTML / internal API Exact (4.3, etc.) Review count HTML / internal API Exact Review text Internal API (paginated) Exact Review date Internal API Exact timestamp App version reviewed on Internal API Exact Developer reply Internal API Exact If your use case needs exact install counts, you’ll need to cross-reference with third-party analytics providers like AppFollow, Sensor Tower, or data.ai — Play’s own data won’t give you them.
The google-play-scraper library (fastest path)
The
google-play-scraperPython package wraps Google Play’s internal_/PlayStoreUi/data/batchexecuteendpoint and handles pagination and parsing for you. it’s the fastest way to get moving:from google_play_scraper import app, reviews, Sort # app metadata (includes installs range + rating) result = app( 'com.spotify.music', lang='en', country='us' ) print(result['installs']) # "1,000,000,000+" print(result['score']) # 4.3 print(result['ratings']) # 35821944 # paginated reviews result, continuation_token = reviews( 'com.spotify.music', lang='en', country='us', sort=Sort.NEWEST, count=100, filter_score_with=None ) # keep fetching with the token result2, next_token = reviews( 'com.spotify.music', continuation_token=continuation_token )The library handles the protobuf decoding that Google’s batchexecute endpoint returns, which saves you the pain of doing it manually. You can pull 200-300 reviews per call before Google starts throttling. For full review dumps on a popular app, budget 10-20 requests with 2-3 second delays between them.
One gotcha: the
countparameter is a hint, not a guarantee. Google sometimes returns fewer results per page, especially for older apps with sparse reviews.Hitting the internal API directly
If you want more control (or the library breaks after a Play Store update), you can hit the batchexecute endpoint yourself. this is the same approach you’d use when reverse-engineering mobile app APIs for data extraction — identify the real endpoint, replicate the payload, strip out the obfuscation.
The request structure for Play reviews looks like this:
import requests, json url = "https://play.google.com/_/PlayStoreUi/data/batchexecute" payload = { "f.req": json.dumps([[["UsvDTd","[[null,[[10,[10,50]],true,null,[96,27,4,8,57,30,110,79,11,16,49,1,3,9,12,104,55,56,51,10,34,31,77,49,28,28,7,9,5,10,58,68,45,35,51,51,8,22,45,20,13,47,8,77]],[[\"en\",\"us\"]],null,null,null,[[]]]]",None,"generic"]]])) } headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } r = requests.post(url, data=payload, headers=headers)The response is a nested JSON-within-string format (Google wraps it in
)]}'\n). You’ll need to strip that prefix, then parse through 2-3 layers of arrays to get to the review data. annoying, but doable.If you need to intercept and modify these requests to understand the full parameter set, comparing Charles Proxy vs mitmproxy for mobile API scraping covers how to set up a MITM proxy to capture the exact payloads Play Store apps send.
Scaling up: proxies and rate limits
Scraping a single app is easy. Scraping 10,000 apps for a market survey is where things break. Google Play enforces rate limits per IP aggressively — you’ll hit 429s after roughly 50-80 requests from the same IP in a short window.
Here’s a practical setup for scale:
- Use rotating residential proxies (datacenter proxies get blocked faster)
- Keep delays between 2-5 seconds per IP
- Rotate user-agent strings alongside IPs
- Implement exponential backoff on 429 responses
- Cache app metadata aggressively (install counts don’t change hourly)
For review monitoring at scale, consider pulling just new reviews using the
Sort.NEWESTparameter and a watermark timestamp. pulling all reviews on every run is wasteful and gets you blocked faster.Some apps also serve different content by country. the
countryparameter matters: an app with 100 English reviews might have 5,000 Japanese ones. if you’re doing global sentiment analysis, you need to loop across country codes.When the API breaks: HTML scraping fallback
Google occasionally changes the batchexecute payload structure, and libraries take a few days to catch up. The HTML fallback is slower but more stable for app metadata (installs, rating, description):
from bs4 import BeautifulSoup import requests app_id = "com.spotify.music" url = f"https://play.google.com/store/apps/details?id={app_id}&hl=en&gl=us" headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"} r = requests.get(url, headers=headers) soup = BeautifulSoup(r.text, "html.parser") # install count is in a specific itemprop or data-g-label span installs = soup.find("div", {"data-g-label": "Installs"})The HTML structure shifts more often than you’d like. If you need something truly stable for a production pipeline, combining the API approach for reviews with HTML scraping for metadata hedges against breakage in either.
If Play Store starts serving JavaScript-rendered content that breaks requests-based scraping, Playwright with a real browser context is the nuclear option. it’s slow and expensive at scale but handles any anti-bot measure short of device attestation. For mobile-specific SSL pinning issues when working through a proxy, Frida vs Objection for bypassing mobile app SSL pinning is worth reading before you go down that path.
Handling pagination and completeness
Getting all reviews for a popular app (some have millions) requires careful pagination handling:
continuation_tokenexpires after roughly 24 hours- The API caps total returnable reviews at around 4,000-5,000 per language/country/sort combination
- Sorting by
Sort.MOST_RELEVANTandSort.NEWESTgives you different slices of the total review pool - There’s no way to get a full 100% complete dump through the API alone
For deeper coverage, the pillar guide on scraping Google Play reviews covers additional approaches including combining API results with third-party data sources.
One approach for completeness: pull NEWEST reviews on a daily cron, sort by date, and store incrementally. over time you build a more complete dataset than any single bulk pull would give you.
Bottom line
google-play-scraperis the right starting point for 90% of use cases — it handles protobuf decoding and pagination so you don’t have to. for scale, pair it with rotating residential proxies and incremental pulls by date. if exact install numbers matter for your analysis, augment with a paid analytics provider because Play Store won’t give them to you. DRT covers these mobile scraping tradeoffs in more depth across the rest of the mobile-scraping category. -
Scraping concert and event ticket pricing
Scraping concert and event ticket pricing
Scrape event ticket pricing and you tap into one of the most volatile pricing datasets in commercial scraping. Concert and sports ticket prices change minute-by-minute on the secondary market, with the same seat rotating through several listings per day during peak demand events. The scraping landscape is shaped by three things: the dominant secondary marketplaces (StubHub, SeatGeek, Vivid Seats, Tickets.com) each with aggressive bot defenses, the primary marketplaces (Ticketmaster, AXS) that gate inventory behind queue systems and bot challenges, and a per-event search dimensionality that creates substantial coverage challenges for any scraper trying to cover a full season of an MLB or NBA team across all opponents and seat sections.
This guide focuses on practical patterns for analytical use cases like pricing intelligence, demand forecasting, and resale arbitrage research. The patterns transfer across U.S. and European ticket aggregators with appropriate per-market adjustments.
Source taxonomy and event identifiers
The event ticketing ecosystem has three distinct source types.
Primary marketplaces (Ticketmaster, AXS, See Tickets, Eventbrite) sell tickets directly from venues and promoters. They expose event detail pages with seat-section-level inventory but enforce queue-based access for high-demand on-sales and aggressive bot defenses to prevent scalping. The data is the canonical “starting price” for any event.
Secondary marketplaces (StubHub, SeatGeek, Vivid Seats, TickPick) facilitate resale of tickets between buyers and sellers. They aggregate listings from individual sellers and broker accounts. The pricing data is dramatically more dynamic than primary because resellers reprice continuously based on demand signals.
Aggregator search engines (Gametime, FanGuide, BetterEvents) layer search across multiple secondary marketplaces. These tend to be the easiest scraping targets because their business model is itself based on aggregating public data.
Every event has a primary marketplace event identifier (usually a Ticketmaster event ID) and per-secondary-marketplace identifiers that map to the same physical event. Cross-source deduplication uses the venue plus event date plus performer as the canonical join key.
import httpx SEATGEEK_HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json", } async def search_seatgeek_events(performer_id: int, proxy: str): url = "https://api.seatgeek.com/2/events" params = { "performers.id": performer_id, "per_page": 50, "sort": "datetime_local.asc", } async with httpx.AsyncClient(proxy=proxy, headers=SEATGEEK_HEADERS, timeout=20) as c: r = await c.get(url, params=params) if r.status_code == 200: return r.json().get("events", []) return []SeatGeek has a public developer API that handles event discovery and basic pricing. For deeper listing-level data (individual seats, real-time prices), you have to scrape the public web pages because the API exposes only aggregate stats.
Event-driven scrape scheduling
Ticket pricing has a distinct lifecycle that drives scrape scheduling. The on-sale moment is the highest information-density window: prices set at on-sale anchor the entire pricing arc. The 30-day window before the event sees the steepest pricing changes as demand becomes clear. The 24-48 hours before the event sees the highest price-change frequency as resellers fire-sale unsold inventory.
Optimal snapshot frequency aligned to lifecycle:
Window Frequency Pre-on-sale Daily On-sale day Every 30 minutes 30+ days out Daily 7-30 days out Twice daily 1-7 days out Hourly Day of event Every 30 minutes This frequency-by-lifecycle approach optimizes proxy spend against analytical signal. Constant high-frequency snapshotting wastes resources during the long quiet window 30+ days out.
Section and price-tier normalization
Venues publish seating in section names that vary widely (Upper Deck 405, Loge 200 Section A, Grand Tier Box 4). For analytics, normalize section to a price-tier classification: Floor/Court, Lower Bowl, Mezzanine, Upper Deck, Behind-the-stage. Each venue has its own section-to-tier mapping that you build once and cache.
def section_to_tier(venue_id: str, section_name: str) -> str: mapping = SECTION_TIER_MAPPINGS[venue_id] return mapping.get(section_name.upper(), "unknown")For sports venues (where section layouts are stable across the season), the mapping is straightforward. For touring concerts (where the same venue can have different floor configurations per show), the mapping is event-specific and requires per-event setup.
Schema for ticket listing snapshots
CREATE TABLE ticket_listing_snapshot ( snapshot_at TIMESTAMP NOT NULL, event_id VARCHAR(64) NOT NULL, source VARCHAR(16) NOT NULL, listing_id VARCHAR(128) NOT NULL, section VARCHAR(64), row VARCHAR(16), quantity INT, price_each_usd DECIMAL(10,2), price_tier VARCHAR(32), deal_score DECIMAL(5,2), PRIMARY KEY (snapshot_at, event_id, source, listing_id) );For broader pattern guidance, see our residential proxy provider ranking and our headless browser frameworks ranking.
Detecting and routing around bot challenges
When ticket marketplaces flag your traffic, the response is usually a Cloudflare or vendor interrogation page rather than a clean HTTP error. Your scraper needs to detect this content-type swap explicitly. Look for the signature
cf-mitigatedheader, the presence of__cf_chl_cookies, or HTML containingJust a moment....def is_challenged(response) -> bool: if response.status_code in (403, 503): return True if "cf-mitigated" in response.headers: return True if "__cf_chl_" in response.headers.get("set-cookie", ""): return True body = response.text[:2000].lower() return "just a moment" in body or "checking your browser" in bodyWhen you detect a challenge, do not retry on the same IP for at least 30 minutes. Mark that IP as cooling and route subsequent requests to a different IP in your pool. Aggressive retries on a flagged IP cause the cooling window to extend. For pages that absolutely must be fetched, have a fallback path that uses a headless browser. Most production setups maintain a 95/5 split between the lightweight HTTP path and the browser fallback path.
Operational monitoring and alerting
Every production scraper needs three monitoring layers. The first is per-IP success rate over a 5-minute window, alerting if any IP drops below 80%. The second is parser error rate, alerting if more than 1% of fetched pages fail to extract the canonical fields. The third is data freshness, alerting if your downstream consumers see snapshots more than 24 hours old.
import time from collections import deque class IPHealthTracker: def __init__(self, window_seconds: int = 300): self.window = window_seconds self.events = {} def record(self, ip: str, success: bool): bucket = self.events.setdefault(ip, deque()) now = time.time() bucket.append((now, success)) while bucket and bucket[0][0] < now - self.window: bucket.popleft() def success_rate(self, ip: str) -> float: bucket = self.events.get(ip) if not bucket: return 1.0 return sum(1 for _, ok in bucket if ok) / len(bucket)Wire this into Prometheus or your existing observability stack so the on-call engineer sees IP degradation as it happens rather than after the daily snapshot fails. For long-running operations, IP rotation triggered by the health tracker is more reliable than fixed rotation schedules.
Pipeline orchestration and scheduling
For any non-trivial event ticketing scraping operation, a dedicated orchestration layer is the difference between a script you babysit and a service that runs unattended. The two strong open-source choices in 2026 are Prefect 3 and Dagster.
from prefect import flow, task @task(retries=3, retry_delay_seconds=60) def fetch_source(source_id: str, page: int): return crawl_one_page(source_id, page) @flow(name="event-ticketing-daily-sweep") def daily_sweep(source_ids: list): futures = [] for sid in source_ids: for page in range(1, 30): futures.append(fetch_source.submit(sid, page)) return [f.result() for f in futures]Run the flow on a cadence aligned to how dynamic the underlying data is. For event ticketing where records change intraday, a 4-6 hour cadence catches meaningful movements. For longer-cycle data, daily is sufficient.
Data quality monitoring patterns
Beyond per-IP success rate, every snapshot should pass a small battery of data quality checks before being considered authoritative. Structural checks verify that every required field is present and of the expected type. Distributional checks compare the current snapshot against recent history. Semantic checks compare related fields for consistency.
def quality_check(snapshot: list[dict]) -> list[str]: errors = [] if not snapshot: errors.append("empty snapshot") return errors avg_yesterday = get_yesterday_avg_size() if len(snapshot) < avg_yesterday * 0.7: errors.append("snapshot size below threshold") return errorsRun quality checks as a separate flow that gates promotion of the snapshot from staging to production. A snapshot that fails quality checks should be quarantined for human review.
Cost optimization strategies
Proxy bandwidth is usually the dominant cost in a production scraping operation. Three optimization patterns consistently reduce cost without hurting data quality. The first is request deduplication: serve cached responses when consumers ask for the same record within the same hour. The second is conditional GET using ETag or If-Modified-Since headers when supported. The third is selective field hydration when the upstream API supports field selection.
For workloads above 100 GB of monthly proxy bandwidth, these three optimizations together reduce cost by 40-60% without changing the analytical output. The engineering effort is modest and the payback period is usually under a month at production volume.
End-to-end pipeline architecture
A production-grade scraping pipeline has four layers: collection, parsing, storage, and serving. The collection layer handles the network conversation and knows nothing about data shape. The parsing layer transforms raw bytes into structured records and owns the schema. The storage layer holds the canonical snapshots in a query-optimized format like DuckDB or ClickHouse. The serving layer exposes the data to consumers and should be denormalized and pre-aggregated where possible. Decoupling these layers also enables independent scaling.
Legal and compliance considerations
Public event ticketing data is generally treated as fair to scrape in most jurisdictions, but always confine your collection to non-personal data. For commercial deployment, document your basis for processing, your data retention period, and your purpose limitation. The W3C Web Annotation guidance and similar published frameworks remain useful starting points for documenting your approach.
Sample analytics queries
-- Volume trend over the last 30 days SELECT date_trunc('day', snapshot_at) AS day, COUNT(*) AS records FROM snapshot WHERE snapshot_at > now() - interval '30 days' GROUP BY 1 ORDER BY 1; -- Source distribution SELECT source, COUNT(*) AS records FROM snapshot WHERE snapshot_at > now() - interval '7 days' GROUP BY source ORDER BY records DESC;Add a category share view, a source concentration view, and a price-volatility view (where applicable) and you have a solid foundation for a event ticketing intelligence product.
Versioning your scraper for source evolution
Every event ticketing source evolves its schema regularly. Stamp every snapshot row with the scraper version that produced it. Downstream analytics can filter by version when they need consistent semantics across a time range. Pair this with a small registry table that documents what each scraper version did differently so debugging unexpected metric jumps becomes tractable.
Caching strategy and incremental crawls
Full daily snapshots scale linearly with source size, which becomes expensive at multi-million record scale. Most production deployments shift from full snapshots to incremental refreshes after the initial ramp using freshness deadline, volatility, and business priority signals. Priority-driven scheduling reduces total request volume by 60-80% compared to blind full snapshots.
Building a deal-finder dashboard
The most common analytical product on top of ticket scraping is a deal-finder that flags listings with prices below market for their seat tier. The deal score is computed as the percentile rank of a listing’s price within all current listings of the same section-tier and quantity for the same event.
def deal_score(listing, event_listings): same_tier = [l for l in event_listings if l['price_tier'] == listing['price_tier']] if not same_tier: return 50.0 rank = sum(1 for l in same_tier if l['price_each_usd'] < listing['price_each_usd']) return 100.0 * rank / len(same_tier)A listing in the bottom 10th percentile is a notable deal. Combined with a freshness filter (listing posted within the last hour), this is the foundation for a real-time deal-alerting product.
Demand forecasting from scraped data
Aggregating ticket scrape data across hundreds of events reveals demand patterns that inform forecasting models. The most useful features are: average asking price per section-tier 30 days out, listing count 30 days out, and the rate of new listings appearing per hour. These features predict same-event sell-through with reasonable accuracy.
For a venue with hundreds of events per year, a forecasting model trained on historical scrape data outperforms simple seasonal models substantially. The training data accumulates naturally as you snapshot continuously.
Cross-platform price spread analytics
The same ticket often appears at different prices on different secondary marketplaces because brokers list at different markups across channels. Tracking the cross-platform price spread per listing reveals broker channel strategy. A broker that consistently lists higher on StubHub than on SeatGeek is using StubHub as their premium channel; a broker that lists lower on TickPick is using TickPick as their volume channel.
For arbitrage research, the cross-platform spread itself is the alpha signal. Plus the time-derivative of the spread (how it changes minute-by-minute) reveals the platform’s freshness and the broker’s repricing cadence.
Working with hosted scraping services
For projects where the engineering investment of running a self-hosted scraping pipeline is not justified, hosted scraping services like ScrapingBee, ZenRows, ScrapeOps, and Apify offer a different cost-and-control tradeoff. These services maintain proxy pools and headless browser fleets and expose a per-request API that abstracts away the infrastructure.
The cost model is per-request rather than per-byte. For low-volume projects (under 100,000 requests per month), the hosted services are typically cheaper than rolling your own proxy and browser infrastructure. For high-volume projects, the math flips because the per-request markup adds up at scale.
import httpx async def scrape_via_hosted(target_url: str, api_key: str): proxy_url = f"https://api.scrapingbee.com/api/v1/?api_key={api_key}&url={target_url}&render_js=true" async with httpx.AsyncClient(timeout=60) as c: r = await c.get(proxy_url) return r.textFor research projects with bounded scope, the hosted-service path is often the fastest way to ship. For ongoing production pipelines, the self-hosted path tends to win on per-request cost and on long-term flexibility.
Long-term archival and data retention
Snapshot data accumulates rapidly. A daily snapshot of even a moderate-sized dataset produces gigabytes per month and terabytes per year. The storage layer needs a clear lifecycle policy. Hot data (last 90 days) sits in your primary store for fast queries. Warm data (90 days to 2 years) sits in a cheaper columnar archive (Parquet on S3, BigQuery, ClickHouse cold storage). Cold data (older than 2 years) sits in compressed archive form, accessed rarely.
def lifecycle_archival(snapshot_age_days): if snapshot_age_days <= 90: return "hot" elif snapshot_age_days <= 730: return "warm" else: return "cold"The lifecycle policy interacts with your data retention obligations. Some jurisdictions impose maximum retention periods on certain data types. Document the retention policy in writing and audit compliance quarterly.
International event ticketing notes
Outside the U.S., the dominant secondary marketplaces shift but the patterns transfer. Viagogo dominates Europe and Asia, twickets handles fan-to-fan UK resales, and a long tail of country-specific marketplaces (Festicket for European festivals, Tixsa in South Africa) handle regional events. Each has its own bot defense profile and its own URL patterns, but the canonical fields (event, date, section, row, quantity, price, currency) are universal.
For multi-region pipelines, build a per-region adapter pattern with a shared canonical schema. The shared schema is the integration point; the adapters handle source-specific quirks like UK postcode-based delivery zones or European VAT-inclusive pricing.
European tickets carry an additional layer of consumer protection rules, including the EU Consumer Rights Directive that limits resale price markups in some member states. The pricing data scraping is fair, but commercial deployment of resale-price intelligence in EU markets needs specialized counsel.
Common pitfalls when scraping event ticket prices
Three issues dominate ticket-market scrapers. The first is row-level vs section-level averaging. The same section (e.g., Section 119) often holds tickets at $80 in row 22 and $240 in row 1. Aggregating to section level smears the price signal. Capture row when the secondary market exposes it (StubHub, SeatGeek do for most NBA/NFL events) and store the section-level summary as a derived view.
The second is fee-inclusive vs fee-exclusive display. The displayed price often excludes fees, which can add 20-40% at checkout. The ‘Worry-Free’ or ‘All-In’ price toggle changes the displayed value mid-session. Always pull the fee-inclusive total or compute it from the breakdown.
The third is dynamic-pricing artifact contamination. Ticketmaster’s dynamic pricing layer reprices high-demand events in real time. A snapshot taken during a pricing pulse shows a transient price that is not representative of the session. Take 3-5 snapshots within a 15-minute window and use the median to filter dynamic-pricing noise.
FAQ
Is scraping ticket prices legal?
Public ticket listings are generally considered public commercial information. The marketplaces have terms of service that prohibit unauthorized scraping; their enforcement focuses on commercial competitors and on scalpers. Confine your collection to non-personal data and consult counsel for commercial use cases.What about Ticketmaster’s queue system on high-demand on-sales?
Ticketmaster Verified Fan and the queue systems are explicitly designed to prevent bot access. Bypassing these for ticket-buying purposes violates the BOTS Act in the U.S. and similar laws in other jurisdictions. For analytical scraping of price data after on-sale, the standard event detail pages remain accessible without queue interactions.Can I scrape secondary marketplace listings at scale?
Yes, with appropriate proxies and rate limits. StubHub and SeatGeek both have moderate bot defenses that respond well to U.S. residential IPs and reasonable request rates. Vivid Seats is somewhat more aggressive.How do I track sold tickets vs. active listings?
Sold listings disappear from the marketplace search. By comparing consecutive snapshots, you can identify listings that sold (disappeared) and at what price they were last shown. This sold-listing-derivation is the foundation of marketplace analytics.What about price-floor and price-ceiling rules?
Ticketmaster and the major leagues enforce price floors on certain ticket types (resale below face value sometimes restricted by team policy). The price floor data is published per event and is useful context for resale-pricing analytics.Is reselling scraped ticket-price data legal in 2026?
Aggregate market analytics fall in a defensible zone post-hiQ for public listings. Reselling individual tickets or contact data acquired by scraping is a different regulatory surface and is restricted in many states.How do I track price drops as the event approaches?
Sample every 6-12 hours for events 14-90 days out, hourly inside the final week, and every 5-15 minutes on the day of the event when prices move most.To build broader event intelligence pipelines, browse the ecommerce scraping category for tooling reviews and framework deep dives.