--- name: binance-market-data-scraper description: Read Binance spot market data as structured records via the Apify Actor arman-bd/binance-market-data-scraper. Every record carries the 24h ticker rollup for a pair (last price, absolute and percentage change, high/low, base and quote volume, trade count) and optionally attaches OHLCV candles, order-book depth or the most recent public trades. Use when a task needs candle history for a backtest, a spread and liquidity snapshot, a scheduled price panel, or a trade tape across many pairs in one call. Not for futures, options, margin, account balances or order placement, and not a websocket: each run is a point-in-time snapshot. --- # Binance Market Scraper Apify Actor `arman-bd/binance-market-data-scraper`. Give it a list of spot symbols, get one dataset record per symbol. The 24h ticker is always present; `dataType` decides which one extra block (candles, order book or trades) rides along. It runs without credentials and there is nothing to authenticate. ## When to use it - Pull OHLCV candles for a universe of pairs to seed or extend a backtest. - Snapshot the top of book for several pairs on a schedule to track spread and depth. - Feed a price panel: one ticker sweep gives last price, 24h change, high, low and volume. - Capture a recent trade tape for microstructure or taker-flow work. - Watch a watchlist over time by scheduling the run and diffing `lastPrice` and `quoteVolume` between runs. ## When not to use it - Futures, options, margin or any other derivative market. This is spot only. - Anything account-scoped: balances, open orders, fills, withdrawals. No credentials are accepted and none of that is returned. - Tick-by-tick streaming. Order books and trades come back as a snapshot at the moment of the run, so poll on a schedule rather than expecting a live feed. - Cross-exchange price comparison. One exchange, one venue. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/binance-market-data-scraper').call({ symbols: ['BTCUSDT', 'ETHUSDT', 'SOLUSDT'], dataType: 'klines', interval: '1h', limit: 500, startTime: '2026-08-01', }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~binance-market-data-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"symbols":["BTCUSDT","ETHUSDT"],"dataType":"ticker"}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/binance-market-data-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `symbols` | string[] | no | `["BTCUSDT","ETHUSDT"]` | Spot symbols. Everything that is not a letter or a digit is stripped and the rest is upper-cased, so `BTC/USDT`, `btc-usdt` and `BTCUSDT` are one and the same input. Duplicates are removed. An empty list falls back to the two defaults. | | `dataType` | string | no | `"ticker"` | One of `ticker`, `klines`, `depth`, `trades`. Decides which extra block is attached. Anything else throws before the first request. | | `interval` | string | no | `"1h"` | Candle size. One of 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M. Only read when `dataType` is `klines`, and only validated then. | | `limit` | integer | no | `100` | Rows per symbol. Clamped, not rejected: the ceiling is 1000 for `klines` and `trades`, 5000 for `depth`, and 1 for `ticker`. Values below 1 become 100. | | `startTime` | string | no | `""` | Candles from this point forward. An ISO date such as `2026-01-01`, or an epoch timestamp in seconds or milliseconds (anything above 1e11 is read as milliseconds). Only applies to `klines`; supplied for another type it is logged and ignored. An unparseable value throws. | **`dataType` is the whole decision.** It costs the same one extra request per symbol whichever of the three you pick, so the trade-off is not cost but shape: you get exactly one of candles, book or tape per run, and the other two come back `null`. If you need two of them for the same pair, run the Actor twice and join on `symbol`. `interval`, `limit` and `startTime` are inert unless the type they belong to is selected, so leaving them at their defaults on a `ticker` sweep is correct rather than lazy. ## Output One record per symbol whose ticker resolved. Prices, volumes and percentages are numbers rather than the strings the source sends; every timestamp is ISO 8601. | Field | Type | Notes | |---|---|---| | `symbol` | string | As the exchange reports it, so always the bare upper-case form. Join back to your input on this. | | `lastPrice` | number \| null | Most recent trade price. | | `priceChange` | number \| null | Absolute move over the rolling 24h window. | | `priceChangePercent` | number \| null | Percentage move over the same window, already a number (`0.782` means 0.782%, not 78.2%). | | `weightedAvgPrice` | number \| null | Volume-weighted average price across the window. | | `highPrice` | number \| null | 24h high. | | `lowPrice` | number \| null | 24h low. | | `volume` | number \| null | Base-asset volume over the window. | | `quoteVolume` | number \| null | Quote-asset volume over the window. This is the one to rank liquidity on. | | `openTime` | string \| null | Start of the rolling 24h window. Not midnight: it is 24 hours before the run. | | `closeTime` | string \| null | End of the rolling window, effectively the run instant. | | `tradeCount` | number \| null | Trades inside the 24h window. | | `bids` | object[] \| null | Order-book bids, best first, only when `dataType` is `depth`. Keys: `price`, `qty`. | | `asks` | object[] \| null | Order-book asks, best first, same shape. | | `klines` | object[] \| null | Candles, oldest first, only when `dataType` is `klines`. Keys: `openTime`, `open`, `high`, `low`, `close`, `volume`, `closeTime`, `quoteVolume`, `tradeCount`, `takerBuyVolume`, `takerBuyQuoteVolume`. | | `trades` | object[] \| null | Recent public trades, only when `dataType` is `trades`. Keys: `id`, `price`, `qty`, `quoteQty`, `time`, `isBuyerMaker`. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record from a `klines` run, the candle array trimmed to one entry: ```json { "symbol": "BTCUSDT", "lastPrice": 64584.01, "priceChange": 501.09, "priceChangePercent": 0.782, "weightedAvgPrice": 64616.58408739, "highPrice": 65025.22, "lowPrice": 63880, "volume": 12407.52427, "quoteVolume": 801731835.3088007, "openTime": "2026-08-05T11:29:41.001Z", "closeTime": "2026-08-06T11:29:41.001Z", "tradeCount": 2314415, "bids": null, "asks": null, "klines": [ { "openTime": "2026-08-01T00:00:00.000Z", "open": 62887.88, "high": 63024.04, "low": 62887.87, "close": 62938.01, "volume": 517.26897, "closeTime": "2026-08-01T00:59:59.999Z", "quoteVolume": 32568037.4802799, "tradeCount": 40193, "takerBuyVolume": 232.52413, "takerBuyQuoteVolume": 14639741.9602915 } ], "trades": null, "scrapedAt": "2026-08-06T11:29:41.480Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "host": "https://…", "dataType": "klines", "symbolsRequested": 4, "symbolsFailed": 1, "failures": [ { "symbol": "NOPEUSDT", "error": "rejected by Binance: Invalid symbol. (code -1121)" } ], "seriesSaved": 3, "peakRequestWeight": 14, "requestWeightLimit": 6000, "filters": { "symbols": ["BTCUSDT", "ETHUSDT", "SOLUSDT", "NOPEUSDT"], "dataType": "klines", "interval": "1h", "limit": 5, "startTime": "2026-08-01T00:00:00.000Z" }, "finishedAt": "2026-08-06T11:29:43.255Z" } ``` `seriesSaved` short of `symbolsRequested` is the partial-run signal, and `failures` names every symbol that did not make it with the exact rejection text. Note that `filters.symbols` is the resolved list after normalisation and de-duplication, so it is what actually ran, not what you typed. `host` records which public mirror answered and is only worth looking at when diagnosing a region block. `peakRequestWeight` against `requestWeightLimit` tells you how much headroom a bigger symbol list has: a five-symbol candle run peaks in the teens. ## Behaviour to plan around - **Exactly one extra block is populated per record.** `klines`, `bids`/`asks` and `trades` are all present as keys on every record, and the two you did not ask for are `null`. A `null` here means "not requested", never "no data". - **`limit` is clamped silently.** Ask for 5000 candles and you get 1000, with no error and no note in `RUN_SUMMARY` beyond `filters.limit` showing the clamped value. Read that field back rather than assuming your number survived. - **The 24h window is rolling.** `openTime` and `closeTime` bracket the last 24 hours from the run instant, so two runs an hour apart cover overlapping windows. Do not treat consecutive ticker records as disjoint daily buckets. - **One bad symbol never aborts the run.** An unknown pair is rejected by the source and lands in `RUN_SUMMARY.failures` with its error text. The Actor only throws when every symbol failed. - **The Actor fails over between public mirrors.** A host that answers with a region block is skipped for the next one without burning a retry, and the run then sticks to whichever host answered. Transient errors get three attempts with linear backoff. - **A run has a request-weight budget of 6000 per minute.** The Actor tracks its own usage and pauses for 60 seconds once it passes 4800. Normal runs are nowhere near this; a very long symbol list is the only way to reach it, and the cost is wall-clock time, not lost records. - **Symbols are normalised but not verified locally.** `BTC/USDT` becomes `BTCUSDT` before anything is requested; whether that pair is listed is decided upstream, so a typo shows up as a failure rather than a validation error. - **`startTime` walks forward, not backward.** Each run returns at most 1000 candles from `startTime`, so deep history needs several runs with an advancing `startTime` rather than one run with a bigger `limit`. - **Requests are paced** at roughly 120 ms between symbols, so a 50-symbol sweep takes seconds, not milliseconds. ## Recipes **Seed a backtest with daily candles.** One run per pair batch, from a fixed start. ```json { "symbols": ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"], "dataType": "klines", "interval": "1d", "limit": 1000, "startTime": "2024-01-01" } ``` Take the last `klines[].openTime` you received and pass it as the next run's `startTime` to walk forward without gaps. **Spread and liquidity snapshot.** Schedule it; each run is a point in time. ```json { "symbols": ["BTCUSDT", "ETHUSDT"], "dataType": "depth", "limit": 50 } ``` Spread is `asks[0].price - bids[0].price`; sum `qty` over the first N levels for depth at a distance from mid. **Price panel sweep.** The cheapest shape: one request per symbol, no extra block. ```json { "symbols": ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "ADAUSDT"], "dataType": "ticker" } ``` Render `lastPrice`, `priceChangePercent` and `quoteVolume` straight from the record. **Taker-flow tape.** Recent trades for one or two pairs, on a tight schedule. ```json { "symbols": ["BTCUSDT"], "dataType": "trades", "limit": 1000 } ``` `isBuyerMaker` false means the aggressor was a buyer, so group on it to split taker buy volume from taker sell volume, and de-duplicate across runs on `trades[].id`.