How to scrape salary ranges from Ashby job boards with Python
Pull startup job postings with pay ranges from Ashby boards, annualised to yearly amounts, with currency and remote filters and an only-new feed.
datagrit › Guides › TCGplayer Price Trends & Sales Velocity Tracker
GuideGet 30 and 90-day price change, units sold, sales per day and days of supply for Pokemon, Magic and other TCG cards and sealed products.
Published 2026-10-01 · uses the TCGplayer Price Trends & Sales Velocity Tracker Actor
A card's price today tells you very little on its own. A 15-dollar card that sells forty copies a month and rose 9% in thirty days is a very different item from a 15-dollar card that sold twice in a quarter. Resellers deciding what to buy, collectors timing a sale, store owners pricing singles and people building repricing tools all need the same three things: where the price is going, how fast the item actually sells, and how much stock is sitting on the market.
TCGplayer, a marketplace for trading card games from Pokemon and Magic to Lorcana and One Piece, shows sales history and listings on its product pages. But turning that into a ranked list of risers, fallers and fast movers across a whole set or game means collecting per-printing histories for hundreds of products and doing the arithmetic yourself.
This post covers what TCGplayer exposes, what gets in the way, and how to get computed trend metrics as rows.
The data comes from TCGplayer's public catalogue: its product search and the price history behind each product page. The history is reported in 3-day windows, separately for each printing (Normal, Holofoil, Reverse Holofoil and so on), language and condition, with the market price, units sold and sale price range of each window.
Several details make the raw history awkward to use:
Pokemon cards from one set, priced at least 5 dollars, with at least 10 sales in 30 days, sorted by TCGplayer relevance:
{
"searchQueries": [],
"productLine": "pokemon",
"setNames": ["sv-scarlet-and-violet-151"],
"rarities": [],
"productType": "cards",
"printings": [],
"languages": ["English"],
"minMarketPrice": 5,
"maxMarketPrice": 0,
"minSold30d": 10,
"minSold90d": 0,
"sortBy": "relevance",
"maxProductsToScan": 200,
"maxItems": 300
}
To list risers instead, add "minPriceChange30dPct": 20; for fallers, "maxPriceChange30dPct": -15. productLine covers 25 product lines, including magic, yugioh, lorcana-tcg, one-piece-card-game and pokemon-japan, or all. Leave searchQueries empty to read a whole set or game, or add terms such as charizard or booster box.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run_input = {
"searchQueries": ["charizard"],
"productLine": "pokemon",
"productType": "cards",
"minSold30d": 5,
"maxProductsToScan": 100,
"maxItems": 200,
}
run = client.actor("datagrit/tcgplayer-price-trend-tracker").call(run_input=run_input)
rows = [r for r in client.dataset(run["defaultDatasetId"]).iterate_items() if r.get("found")]
for r in rows[:10]:
print(r["productName"], r["variant"], r["marketPrice"], r["priceChange30dPct"], r["soldPerDay"])
curl -X POST \
"https://api.apify.com/v2/acts/datagrit~tcgplayer-price-trend-tracker/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"searchQueries": ["booster box"], "productLine": "pokemon", "productType": "sealed", "maxProductsToScan": 30, "maxItems": 50}'
Each product needs one extra request for its sales history and the Actor pauses between batches, so plan for roughly 6 seconds per 10 products, about a minute for 100.
One row per printing and language of a product:
| Field | Example | Meaning |
|---|---|---|
productName | Charizard GX - 9/68 (#60 Charizard Stamped) | Card or sealed product |
setName | Battle Academy | Set or expansion |
variant / language | Holofoil / English | Printing and language of this row |
marketPrice | 14.57 | Market price, latest 3-day window, main condition |
priceChange30dPct | 8.8 | Change versus about 30 days ago |
priceChange90dPct | 12.1 | Change versus the oldest window of the 90-day history |
sold30d / sold90d | 35 / 138 | Units sold at the main condition |
soldPerDay | 1.53 | Average daily sales over the 90-day history |
daysOfSupply | 54.2 | Active listings divided by average daily sales |
marketPriceLightlyPlayed | 10.67 | Market price of the Lightly Played condition |
Rows also include setCode, cardNumber, rarity, releaseDate, condition, sealed, lowSalePrice90d, highSalePrice90d, lastSaleWindowStart, allConditionsSold90d, prices for Moderately Played, Heavily Played and Damaged, listingsCount, lowestListingPrice, medianListingPrice, productUrl and imageUrl. Note that listing counts, listing prices and daysOfSupply refer to the whole product, not to one printing.
Schedule a weekly run per game you trade with minPriceChange30dPct: 15 and minSold30d: 10, and write the dataset to a Google Sheet. The sales threshold keeps out cards whose price moved on one or two sales.
Run with productType: "sealed" and a term like booster box, then sort by daysOfSupply. A low number means listings would sell out in a few days at the current pace; a high one means plenty of stock.
import pandas as pd
df = pd.DataFrame(rows)
df = df.dropna(subset=["priceChange30dPct", "soldPerDay"])
candidates = df[(df["soldPerDay"] >= 1) & (df["priceChange30dPct"].between(0, 25))].copy()
candidates["lp_discount_pct"] = (1 - candidates["marketPriceLightlyPlayed"] / candidates["marketPrice"]) * 100
cols = ["productName", "variant", "marketPrice", "priceChange30dPct", "soldPerDay", "daysOfSupply", "lp_discount_pct"]
print(candidates.sort_values("soldPerDay", ascending=False)[cols].head(25))
This lists steadily rising, frequently sold printings and shows how much cheaper Lightly Played copies trade.
maxProductsToScan bounds each run.maxItems accordingly.The Actor reads publicly available catalogue and sales information; it does not log in or bypass access controls.
If you price, buy or analyse cards, computed trend and velocity per printing saves most of the spreadsheet work; pricing is pay per result, the Actor is on Apify, and all fields are described in the documentation.
Pull startup job postings with pay ranges from Ashby boards, annualised to yearly amounts, with currency and remote filters and an only-new feed.
Monitor open roles at a list of companies across 8 applicant tracking systems in one schema, with numeric salaries and a daily only-new feed.
Build lists of French companies from the official Sirene register, filtered by net result, revenue, NAF code and location, using Python or cURL.