--- name: kraken-market-scraper description: Read public Kraken spot market data as one record per pair via the Apify Actor arman-bd/kraken-market-scraper. Every record carries the pair's full trading specification (base and quote assets, decimals, order and cost minimums, tick size, status) and optionally a ticker snapshot with bid, ask, last trade, VWAP, volume, trade counts and range, OHLC candles at any supported interval, recent trades and spread history. Use when a task needs live crypto prices, candles for a backtest, execution-cost or liquidity analysis, or nightly treasury marks. Not for order book depth, account balances, order placement, futures or any authenticated endpoint. --- # Kraken Market Scraper Apify Actor `arman-bd/kraken-market-scraper`. Give it a list of Kraken pairs and choose which market-data blocks you want, get one dataset record per pair. No credentials, no signing and no proxy configuration are involved. ## When to use it - Live prices for a basket of pairs: `bid`, `ask`, `lastPrice` and `spread` in one row each. - OHLC history for a backtest, at any interval from one minute to fifteen days. - Execution-cost modelling, where `spreadHistory` plus `tickSize` and `costMin` say what a fill actually costs. - Liquidity screening: sort a wide pair list by `volume24h` and `tradeCount`. - Nightly treasury marks: one `lastPrice` snapshot per pair on your balance sheet. - Cross-venue comparison, by polling this on a schedule and diffing against another source. ## When not to use it - Order book depth beyond the top of book. Only the best bid and ask are returned. - Anything authenticated: balances, open orders, trade history, staking, funding. - Placing, cancelling or simulating orders. This is read-only market data. - Futures, margin-specific or index products. Spot pairs only. - Deep history in a single call. Candles and trades come back in a bounded recent window; walking further back means repeated runs driven by `since`. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/kraken-market-scraper').call({ pairs: ['XBTUSD', 'ETH/EUR', 'SOLUSD'], dataTypes: ['ticker', 'ohlc'], interval: 1440, maxRows: 365, }); 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~kraken-market-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"pairs":["XBTUSD","ETHUSD"],"dataTypes":["ticker","ohlc"],"interval":1440,"maxRows":30}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/kraken-market-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `pairs` | string[] | yes | | Any Kraken spelling: `XBTUSD`, `XBT/USD` or the canonical `XXBTZUSD`. Slashes, spaces and case are normalised. Spellings that resolve to the same pair are fetched once. An empty list throws. | | `dataTypes` | string[] | no | `["ticker"]` | Any of `ticker`, `ohlc`, `trades`, `spread`. Anything else throws before a request is made. An empty list falls back to `ticker`. | | `interval` | integer | no | `60` | Candle width in minutes. Exactly 1, 5, 15, 30, 60, 240, 1440, 10080 or 21600 are accepted; any other value throws, but only when `ohlc` is selected. | | `since` | string | no | `""` | Unix timestamp in seconds, as a string. Returns only `ohlc`, `trades` and `spread` rows after it. Empty means the most recent window. Has no effect on `ticker`. | | `maxRows` | integer | no | `200` | Caps the length of `ohlcCandles`, `recentTrades` and `spreadHistory`, keeping the newest rows. The specification and ticker fields are unaffected. | **`dataTypes` is the whole cost model, and it is multiplicative.** A run costs one request for the pair index plus one request per pair per selected data type: 50 pairs with `ticker` and `ohlc` is 101 requests, made sequentially. The specification fields (`baseAsset` through `tickSize`) ride along on that single index request and are always present, so never select an extra block to reach them. Note that `maxRows` trims the record after the response arrives; it does not make the request cheaper. ## Output One record per unique pair that resolved. Blocks you did not select are present as `null` rather than absent, so the record shape is stable across runs. | Field | Type | Notes | |---|---|---| | `pair` | string | The altname, for example `XBTUSD`. This is Kraken's spelling, not necessarily the string you passed in. | | `krakenPair` | string | Canonical key, for example `XXBTZUSD`. Stable join key. | | `wsname` | string \| null | WebSocket name, for example `XBT/USD`. | | `baseAsset` | string \| null | Readable base symbol, split from `wsname`. | | `quoteAsset` | string \| null | Readable quote symbol. | | `baseAssetId` | string \| null | Kraken's internal base code, for example `XXBT`. | | `quoteAssetId` | string \| null | Kraken's internal quote code, for example `ZUSD`. | | `status` | string \| null | `online` or `post_only`. `post_only` means the pair is not taking taker orders. | | `lotDecimals` | number \| null | Decimal places allowed on volume. | | `pairDecimals` | number \| null | Decimal places allowed on price. | | `costDecimals` | number \| null | Decimal places on notional cost. | | `orderMin` | number \| null | Minimum order size in base units. | | `costMin` | number \| null | Minimum order notional in quote units. | | `tickSize` | number \| null | Minimum price increment. | | `ask` | number \| null | Best ask. `null` unless `ticker` was selected. | | `askLotVolume` | number \| null | Lot volume at the best ask. | | `bid` | number \| null | Best bid. | | `bidLotVolume` | number \| null | Lot volume at the best bid. | | `lastPrice` | number \| null | Price of the most recent trade. | | `lastVolume` | number \| null | Volume of the most recent trade. | | `volumeToday` | number \| null | Volume since midnight UTC. | | `volume24h` | number \| null | Rolling 24-hour volume. | | `volumeWeightedAvgToday` | number \| null | VWAP since midnight UTC. | | `volumeWeightedAvg` | number \| null | Rolling 24-hour VWAP. | | `tradeCountToday` | number \| null | Trades since midnight UTC. | | `tradeCount` | number \| null | Trades over the rolling 24 hours. | | `lowToday` | number \| null | Low since midnight UTC. | | `low24h` | number \| null | Rolling 24-hour low. | | `highToday` | number \| null | High since midnight UTC. | | `high24h` | number \| null | Rolling 24-hour high. | | `openPrice` | number \| null | Today's opening price. | | `spread` | number \| null | `ask` minus `bid`, in quote currency, rounded to 10 places. Computed here, only when both sides are present. | | `spreadPct` | number \| null | `spread` as a percentage of `ask`, rounded to 6 places. So `0.000155` means 0.000155%. | | `ohlcInterval` | number \| null | The interval used, echoed back. `null` unless `ohlc` was selected. | | `ohlcCandles` | object[] \| null | Newest-last candles, keys `time` (ISO 8601), `open`, `high`, `low`, `close`, `vwap`, `volume`, `tradeCount`. | | `recentTrades` | object[] \| null | Newest-last, keys `time`, `price`, `volume`, `side` (`buy` or `sell`), `orderType` (`market` or `limit`). | | `spreadHistory` | object[] \| null | Newest-last tick history, keys `time`, `bid`, `ask`, `spread`, with the spread precomputed. | | `scrapedAt` | string | Run timestamp, ISO 8601 UTC. | A real record, arrays trimmed to one row: ```json { "pair": "XBTUSD", "krakenPair": "XXBTZUSD", "wsname": "XBT/USD", "baseAsset": "XBT", "quoteAsset": "USD", "baseAssetId": "XXBT", "quoteAssetId": "ZUSD", "status": "online", "lotDecimals": 8, "pairDecimals": 1, "costDecimals": 5, "orderMin": 0.00005, "costMin": 0.5, "tickSize": 0.1, "ask": 64562.3, "askLotVolume": 1.6, "bid": 64562.2, "bidLotVolume": 0.052, "lastPrice": 64562.2, "lastVolume": 0.00403978, "volumeToday": 274.26421031, "volume24h": 1286.18325499, "volumeWeightedAvgToday": 64697.53769, "volumeWeightedAvg": 64593.30096, "tradeCountToday": 14063, "tradeCount": 49444, "lowToday": 64372.6, "low24h": 63823, "highToday": 64936.4, "high24h": 64954.6, "openPrice": 64599.3, "spread": 0.1, "spreadPct": 0.000155, "ohlcInterval": 60, "ohlcCandles": [ { "time": "2026-08-06T11:00:00.000Z", "open": 64549, "high": 64572, "low": 64440.5, "close": 64562.2, "vwap": 64530.1, "volume": 18.15301921, "tradeCount": 1035 } ], "recentTrades": null, "spreadHistory": null, "scrapedAt": "2026-08-06T11:39:55.722Z" } ``` ## 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 { "pairsRequested": 3, "pairsFailed": 1, "failures": [ { "pair": "BTCUSD", "error": "unknown asset pair" } ], "seriesSaved": 2, "filters": { "pairs": ["XBTUSD", "ETH/EUR", "BTCUSD"], "dataTypes": ["ticker", "ohlc"], "interval": 1440, "maxRows": 365, "since": null }, "finishedAt": "2026-08-06T11:39:55.900Z" } ``` `pairsRequested` counts the strings you passed, so `seriesSaved` plus `pairsFailed` can be *less* than it when two of your entries were different spellings of one pair. Each entry in `failures` gives the string you passed in `pair` and the reason in `error`, prefixed with the data type that failed, for example `ohlc: EQuery:Unknown asset pair`. An `unknown asset pair` error means the symbol never resolved and cost no request. ## Behaviour to plan around - **Bitcoin is `XBT` on Kraken, not `BTC`.** `BTCUSD` does not resolve and lands in `failures`. The same goes for a few other legacy codes. - **`pair` in the output is Kraken's altname, not your input string.** Pass `XBT/USD` and the record says `XBTUSD`. Join your own tables on `krakenPair`, or map your input through `filters.pairs` in order. - **One failed block loses the whole pair.** The data types are fetched in sequence and the first failure aborts that pair, so a `ticker` that succeeded is discarded when the following `ohlc` fails. Nothing partial is ever written. - **Unknown pairs are rejected locally.** The full pair index is downloaded once at the start of every run, so a typo costs no request. If that one index request fails, the whole run throws before any pair is attempted. - **Errors can arrive inside a successful response.** The Actor checks for them explicitly and retries only the transient ones, meaning rate limits, lockouts and busy or unavailable notices, three times with linear backoff. Everything else fails that pair immediately. - **Pairs are requested one at a time on purpose.** Batching several into one call returns nothing at all when a single symbol is bad, so per-pair requests are what keep one typo from costing you the rest. - **`Today` and 24-hour figures are different windows.** `volumeToday` starts at midnight UTC, `volume24h` is a rolling window. Comparing the two is meaningless just after midnight. - **`spread` and `spreadPct` are computed here**, not published upstream, and only when both `bid` and `ask` came back. Values are rounded so that binary float noise such as `0.09999999` never reaches the dataset. - **`maxRows` keeps the newest rows.** Candles and trades come back in a bounded window per request (roughly 720 candles, 1000 trades), and `maxRows` trims that window from the old end. To go further back, run again with `since` set. - **The run only throws when every pair failed.** Any surviving pair makes the run succeed with a non-empty `failures` list. ## Recipes **Live prices for a watchlist.** The cheapest useful call: one request per pair. ```json { "pairs": ["XBTUSD", "ETHUSD", "SOLUSD", "ADAUSD"], "dataTypes": ["ticker"] } ``` Use `lastPrice` for marks and `spread` divided by `lastPrice` as a liquidity proxy. Note `status`: a `post_only` pair's quotes are not comparable to a normal one's. **A year of daily candles for a backtest.** ```json { "pairs": ["XBTUSD", "ETHUSD"], "dataTypes": ["ohlc"], "interval": 1440, "maxRows": 365 } ``` `ohlcCandles` is newest-last and each `time` is ISO 8601, so it loads straight into a time series without conversion. `ticker` fields will be `null` because you did not ask for them. **Microstructure detail for one pair.** Three blocks, one pair, so four requests. ```json { "pairs": ["XBTUSD"], "dataTypes": ["ticker", "trades", "spread"], "maxRows": 500 } ``` Combine the `side` distribution in `recentTrades` with the `spread` series in `spreadHistory` to estimate what a market order would have paid. **Incremental polling.** Take the newest `time` from the previous run, convert it to unix seconds, and pass it back. ```json { "pairs": ["XBTUSD"], "dataTypes": ["trades"], "since": "1780000000", "maxRows": 1000 } ``` Only rows after that timestamp are returned, so consecutive runs append rather than overlap.