Skip to main content

Free & Open Data API

The same data and math behind this calculator, as plain HTTP endpoints. Daily BTC/USD history back to 2010 and full DCA backtests, in JSON or CSV. No API keys, no accounts, no signup wall.

Why This Is Free

Getting clean Bitcoin price history usually means an API key, a credit card on file, and a rate-limit table. That's a strange gatekeeping ritual for numbers that were public the moment they happened. This site already assembles the series for its own calculator, so republishing it costs almost nothing — and the whole project is open source, so hiding the data behind a key would be theater anyway.

There is deliberately no auth and no rate-limit code. Responses carry CDN cache headers (60 minutes for prices, 30 minutes for backtests), so a repeat of the exact same URL is normally answered from the cache. A URL that differs in any way, even by a parameter the API ignores, is a separate cache entry and is computed on the server. Nothing throttles that, so please don't script loops of unique URLs.

Endpoints

All endpoints answer GET and OPTIONS, send Access-Control-Allow-Origin: * so browser fetch() works from any origin, and return errors as JSON with a plain-English error message. Errors are never cached. Every parameter is optional, and parameters the API does not know are ignored.

GET

/api/v1/prices

Daily BTC/USD prices as JSON: one [utcMidnightMs, usd] pair per UTC day, with a marker for each saying whether it was observed or estimated. Defaults to the full history, 2010-08-18 to today.

ParamAcceptsDefault
from ISO date (yyyy-MM-dd). Earlier than 2010-08-18 is raised to it. Later than today, or later than to, is a 400. Default: 2010-08-18
to ISO date. Later than today is cut back to today. Earlier than 2010-08-18 is a 400. Default: today (UTC)
provider coinbase or kraken. Default: coinbase
curl 'https://btcdollarcostaverage.com/api/v1/prices?from=2020-01-01&to=2020-12-31'

Try it live: /api/v1/prices?from=2024-01-01&to=2024-06-30

The response to that curl on 2026-10-12, with kinds, note and series shortened:

{
  "source": "coinbase",
  "served": "coinbase",
  "fallback": false,
  "stale": false,
  "asOf": "2026-10-12T05:16:19.151Z",
  "seriesFetchedAt": "2026-10-12T05:16:19.000Z",
  "from": "2020-01-01",
  "to": "2020-12-31",
  "count": 366,
  "estimatedCount": 0,
  "kinds": "oooooooooo...",
  "note": "All prices are USD, one value per UTC day, ...",
  "series": [
    [1577836800000, 7174.33],
    [1577923200000, 6945.02]
  ]
}
Response fields
FieldTypeMeaning
source string The provider you asked for, or the default. It does not change when another exchange answers; served does.
served string The exchange whose series this actually is: coinbase or kraken. Differs from the provider you asked for only when fallback is true.
fallback boolean true when the exchange you asked for could not be reached and the other one answered.
stale boolean true when the exchange could not be reached just now and this is a copy of its series the server fetched earlier.
asOf ISO timestamp When this response was generated.
seriesFetchedAt ISO timestamp When the series was fetched from the exchange. A price dated that same UTC day is the latest trade at that moment, not a close.
from, to ISO date The range that was answered, after the clamping described above.
count number Rows in series. Rows are consecutive UTC days. The last one can be a day before to, in the first moments of a UTC day or while stale is true.
estimatedCount number How many of those rows are straight-line estimates.
kinds string One character per row of series, in the same order: o for an observed price, e for a straight-line estimate between two observed prices. A - would mean unknown; the server does not currently send it.
note string What the prices in this response are, in plain English. If the response is a fallback or stale, the note starts by saying so.
series array [utcMidnightMs, usd] pairs, oldest first, unrounded.

Errors: a 400 for a date or provider that can't be read, or a range that lies wholly in the future or before 2010-08-18. A 503 when neither exchange can be reached and the server holds no recent copy, or when the range has no price yet, which happens for today's date in the first moments of a UTC day. The CSV endpoint answers the same way, in JSON.

GET

/api/v1/prices.csv

The same series as a downloadable CSV with date,usd rows, one row per UTC day, ready for a spreadsheet or pandas. Same parameters as /api/v1/prices, plus kinds.

ParamAcceptsDefault
kinds 1 or true adds a third column, kind, holding observed or estimated. 0, false or nothing leaves the body as date,usd. Anything else is a 400. Default: off
curl -o btc-usd-daily.csv 'https://btcdollarcostaverage.com/api/v1/prices.csv?provider=coinbase'

Try it live: /api/v1/prices.csv?from=2024-01-01&to=2024-06-30

The first rows of that file (Coinbase daily closes):

date,usd
2024-01-01,44220.78
2024-01-02,44972.80
2024-01-03,42862.44

The same days with provider=kraken&kinds=1:

date,usd,kind
2024-01-01,43026.24,estimated
2024-01-02,42937.87,estimated
2024-01-03,42849.50,observed

Prices are rounded to cents (four decimals below $1, so the 2010–2011 era doesn't flatten to zero). The JSON endpoint returns unrounded values. The body is pure data with no comment rows, so the provenance travels in response headers: X-Data-Note (the same text as the JSON note), X-Data-Served (the exchange whose prices are in the file), X-Data-Fetched-At (when that series was fetched) and X-Data-Estimated-Rows (how many rows are estimates). All four, and Content-Disposition, are listed in Access-Control-Expose-Headers, so a cross-origin fetch() can read them. The suggested file name is btc-usd-daily-<served>-<from>-<to>.csv.

GET

/api/v1/dca

A full DCA backtest on the same engine, price pipeline and defaults as the on-site calculator: $50 every week for the last 5 years at a 0.5% fee. It reports history only. A purchase dated after today (UTC) is never counted.

ParamAcceptsDefault
amount USD per purchase. 0 or less is a 400. Above 1,000,000,000 is clamped to it. Default: 50
frequency daily, weekly, biweekly, monthly. Default: weekly
startDate ISO date. Earlier than 2010-08-18 is raised to it. Later than today, or later than endDate, is a 400. Default: 5 years before endDate, never earlier than 2010-08-18
endDate ISO date. Later than today is cut back to today. Earlier than 2010-08-18 is a 400. Default: today (UTC)
fee exchange fee as a % of each purchase, 0 to 50. Outside that range is a 400. Default: 0.5
flat flat fee per purchase in USD, 0 to 1,000,000, charged on top of fee. Outside that range is a 400. Default: 0
provider coinbase or kraken. Default: coinbase
startingBtc BTC already held at the start date, 0 or more. Above 21,000,000 is clamped to it. Default: none
startingCost USD cost per BTC of the starting stack, 0 or more, only together with a positive startingBtc. Above 1,000,000,000 is clamped to it. Left out or 0, the stack is costed at the start date’s price. Default: start-date price
esc yearly purchase increase in %, 0 or more. The amount steps up on each 12-month anniversary. Above 200 is clamped to it. Default: 0
currency USD only. Anything else is a 400. Default: USD
mode api only. Anything else, such as the calculator’s manual, is a 400. Default: api

A value that is clamped is echoed in inputs, so you can see what was run. A negative number, a blank or text where a number belongs, or a date that does not exist is a 400. A calculator link's query string can be sent as it is and is held to the same rules, so a link in another currency or at a manual price is a 400.

curl 'https://btcdollarcostaverage.com/api/v1/dca?amount=100&frequency=weekly&startDate=2020-01-01&endDate=2024-12-31&fee=0.5'

Try it live: /api/v1/dca?amount=100&frequency=weekly&startDate=2020-01-01&endDate=2024-12-31

The response to that curl on 2026-10-12, with breakdown cut to its first row and note shortened. Everything valued at today's price moves with the market:

{
  "inputs": {
    "amountUsd": 100, "frequency": "weekly",
    "startDate": "2020-01-01", "endDate": "2024-12-31",
    "feePercent": 0.5, "provider": "coinbase"
  },
  "asOf": "2026-10-12T05:16:20.049Z",
  "currency": "USD",
  "totalInvested": 26100,
  "btcAccumulated": 1.114657706280992,
  "averageCost": 23415.259996794477,
  "currentValue": 93761.04921749973,
  "profit": 67661.04921749973,
  "roiPercent": 259.23773646551626,
  "xirrPercent": 32.25073739818596,
  "maxDrawdownPercent": 69.63294618721186,
  "feesPaid": 130.5,
  "feePercentOfBuy": 0.5,
  "purchaseCount": 261,
  "estimatedPrices": {
    "purchases": 0, "drawdownDays": 0, "drawdownDaysWalked": 1827
  },
  "spotPriceUsd": 84116.45,
  "spotSource": "coinbase",
  "spotAsOf": "2026-10-12T05:16:20.000Z",
  "served": "coinbase",
  "fallback": false,
  "stale": false,
  "seriesFetchedAt": "2026-10-12T05:16:19.000Z",
  "breakdownSampled": false,
  "breakdown": [
    {
      "date": "2020-01-01T00:00:00.000Z",
      "price": 7174.33,
      "invested": 100,
      "totalInvested": 100,
      "accumulated": 0.013868890892947495,
      "totalAccumulated": 0.013868890892947495,
      "portfolioValue": 99.5,
      "fee": 0.5,
      "priceKind": "observed"
    }
  ],
  "note": "All figures are USD. ..."
}
Response fields
FieldTypeMeaning
inputs object The plan that was run: amountUsd, frequency, startDate, endDate, feePercent and provider (the one you asked for). These appear only when they apply: startDateClamped with requestedStartDate, endDateClamped with requestedEndDate, flatFeeUsd, startingBtc, startingCostUsd, annualEscalationPercent.
asOf ISO timestamp When this response was generated. Its UTC date is the “today” that decides which purchases have happened.
currency string Always USD.
totalInvested number Dollars put in: every purchase in full, fees included, plus the cost of a starting stack.
btcAccumulated number BTC bought, plus the starting stack.
averageCost number totalInvested divided by btcAccumulated.
currentValue number btcAccumulated at spotPriceUsd, or at the newest daily price when spotPriceUsd is null. Valued today, also for a window that ended years ago.
profit number currentValue minus totalInvested.
roiPercent number profit as a % of totalInvested.
xirrPercent number or null Money-weighted annualized return from each purchase date to now. null when the first purchase is less than 90 days old, or when no bitcoin was bought.
maxDrawdownPercent number Largest peak-to-trough fall in the value of the stack, checked on every UTC day from the first purchase to the end of the window.
feesPaid number Dollars taken as fees across all purchases, percentage and flat together.
feePercentOfBuy number feesPaid as a % of the money put into purchases. Equal to fee when there is no flat fee.
purchaseCount number Scheduled purchases made. The true total even when breakdown is thinned. A starting stack is not a purchase.
estimatedPrices object or null How much of the result rests on estimated prices: purchases (buys priced on an estimate), drawdownDays (days of the drawdown check priced on an estimate) and drawdownDaysWalked (days it covered). null only if the series reached the engine without its markers, in which case rows carry no priceKind either.
spotPriceUsd number or null The live price used for currentValue. null when neither exchange had a current quote.
spotSource string or null The exchange that quoted it. It is the other exchange when the one you asked for had no current quote.
spotAsOf ISO timestamp or null When it was quoted.
served string Same as on /api/v1/prices: the exchange whose series priced the purchases.
fallback boolean Same as on /api/v1/prices.
stale boolean Same as on /api/v1/prices.
seriesFetchedAt ISO timestamp Same as on /api/v1/prices.
breakdownSampled boolean true when there are more than 2,000 rows and breakdown was thinned.
breakdown array One row per purchase, oldest first. See the next table.
note string These rules and the provenance of the prices, in plain English, for this response. If the response is a fallback or stale, the note starts by saying so.
Each breakdown row
FieldTypeMeaning
date ISO timestamp The purchase day, at 00:00 UTC.
price number The USD price the purchase was made at: the series value for that UTC day, or the newest earlier value when the day has none yet (priceKind is then estimated).
invested number Dollars that left the bank for this purchase, fee included.
totalInvested number Running total of invested.
accumulated number BTC bought: invested minus fee, divided by price.
totalAccumulated number Running total of BTC.
portfolioValue number totalAccumulated at the market price for this row’s date. On a purchase row that is this row’s price.
fee number Dollars of invested taken as fees: fee % of the purchase plus flat, never more than the purchase itself. 0 on a starting-stack row.
priceKind string observed or estimated. Left out on a starting-stack row whose cost you supplied, because that price is yours.
isStartingStack boolean Present, and true, only on the first row when startingBtc was sent. That row is a position, not a purchase: its price is the stack’s cost per BTC, invested its total cost, and accumulated the BTC held. Its portfolioValue is the stack at the start date’s market price, not at a cost you stated.

breakdown is capped at about 2,000 rows. A longer schedule is thinned evenly (breakdownSampled: true), the final purchase is always kept, and the running totals are exact at every row that remains. A flat fee at or above the purchase amount buys no bitcoin: the response reports the money spent and a stack of zero. A 503 means neither exchange could be reached, or the price series does not yet reach the end of the window; every date or value the API cannot use is a 400 that says what to change. The math itself is documented on the methodology page.

The Honest Bits

Where the prices come from. 2010-08-18 through 2015-07-19 are a bundled copy of blockchain.info's market-price series, which blockchain.info describes as an average across major bitcoin exchanges. They are not exchange closes. From 2015-07-20 the series comes from the exchange you pick. Coinbase gives a close for every UTC day; a day it had no candle for would be a straight-line estimate, marked as one. Kraken gives one observed close a week, dated on a Wednesday, and every day in between is a straight-line estimate, not a price that traded. On 2026-10-12 the full history was 5,900 days: none of them an estimate on Coinbase, 3,516 of them on Kraken. kinds and priceKind mark which is which, and every response repeats this in its note.

Dates before 2015-07-20 sit a day early. Each blockchain.info value is stamped 00:00 UTC, and where it can be checked against exchange candles it tracks the close of the day before its date. The exchange values that follow are closes of their own date. If you join this data to another set of daily closes, shift the rows up to 2015-07-19 back one day first. The methodology page has the measurement, and the reason Kraken's series is weekly.

Today's price is not a close. A value dated the current UTC day is the latest trade when the server fetched the series, which it does roughly once an hour; seriesFetchedAt gives the exact time. On Coinbase the day gets its close when it ends. On Kraken the whole week in progress moves: every value dated after the newest completed weekly close lies on a straight line to that latest trade, so it changes at each refetch and once more when the week closes. There, today's value is marked observed, and estimated from tomorrow on unless today is a Wednesday. Kraken values up to the newest completed weekly close do not change.

Outages are flagged, not hidden. If the exchange you asked for can't be reached, you get the copy of its series the server fetched earlier the same UTC day (stale), or failing that the other exchange's series (fallback). With both exchanges down you get the last series the server holds if it is under a day old, and otherwise a 503. A response with either flag set, or a backtest valued without a live quote, is cached for 5 minutes, not the usual lifetime.

USD only. Every number in every endpoint is US dollars. The site's other display currencies are a display-time conversion, and pretending the API had native EUR or JPY series would be dishonest.

Freshness. Price responses are edge-cached for up to 60 minutes and backtests for up to 30 minutes. For 5 minutes after that, the next request still gets the cached copy while a new one is built. asOf says when a JSON response was built; seriesFetchedAt and X-Data-Fetched-At say when its prices were fetched. Don't use these endpoints as a live ticker.

What Changed in October 2026

No field was removed or renamed, and every documented parameter still works. Some numbers moved, and some requests that used to be answered are now refused. If you stored earlier responses you should know which:

  • The default provider is now coinbase. It was kraken. A request with no provider now gets a close for every day where it used to get weekly closes joined by straight lines. Send provider=kraken for the Kraken series.
  • Kraken's weekly closes moved one day earlier. Each was dated on the Thursday its week ended at 00:00 UTC. It is now dated on the Wednesday before, the day whose last trade it is. Every provider=kraken value after 2015-07-19 changed with it.
  • More requests to /api/v1/dca are a 400. A window that starts after today used to get a 503 “try again”, or a 200 in which every figure was zero. A currency other than USD and mode=manual used to be ignored, so a calculator link for ¥5,000 a week was answered as $5,000 a week. A number sent as a blank, such as fee=%20, used to be read as 0.
  • The default startDate changed at two edges. It no longer falls before 2010-08-18, and 5 years back from Feb 29 is Feb 28 (it was Mar 1).
  • With no live quote, currentValue uses the newest daily price. It used to use the last price inside the window, which for a window that ended in the past was not a current value.
  • A response past its cache lifetime is re-served for at most 5 minutes. It was 24 hours, so a caller that asked once a day could be handed the previous day's answer.
  • New fields and parameters. Prices: served, fallback, stale, seriesFetchedAt, estimatedCount, kinds. Backtests: the flat parameter, those four provenance fields, feesPaid, feePercentOfBuy, estimatedPrices, spotSource, spotAsOf, fee and priceKind on each row, and startDateClamped with requestedStartDate in inputs. CSV: the kinds parameter and three more X-Data- headers, all readable from a browser.

Fair Use

No keys and no quotas means this runs on good faith. Cache responses on your side, fetch the full history once instead of a thousand slices, and don't poll faster than the cache windows above — you'd only be re-downloading identical bytes. If you build something on top of this, a link back to btcdollarcostaverage.com is appreciated but not required. If your use case needs more than this (bulk, streaming, something odd), get in touch — the answer is probably yes.

Open Source

The entire site, these endpoints included, is MIT-licensed. Don't trust the numbers? Read the code that produced them, or run your own copy:

github.com/9drix9/bitcoindca-calculator

Prefer Buttons to curl?

Everything the API computes, the calculator shows with charts. Same data, same math.

Open the Calculator

Disclaimer: This API serves an educational simulation and historical market data, not investment advice. Historical and simulated performance does not guarantee future results. The data is provided as-is, with no uptime or accuracy guarantees. Bitcoin is volatile and carries significant risk.