Skip to content
DonutStatsAPI
Sign up
Concepts

Historical data

Nothing DonutStats records is thrown away. Every dataset has a history endpoint you can page through from today back to day one.

Live endpoints show the market and the leaderboards as they are right now. History endpoints show everything that came before. All of it is available on every plan.

DatasetEndpointWhat you get
Closed listings/v1/auction/historyEvery listing that left the auction house
Closed orders/v1/orders/historyEvery buy order that closed, filled or not
Fills/v1/orders/fillsEvery delivery to a buy order and what it paid
Prices/v1/items/{key}/historyHourly and daily prices for every product
Player stats/v1/players/{username}/historyEvery reading of every stat
Player ranks/v1/players/{username}/ranksA player's rank on every snapshot
Leaderboards/v1/leaderboards/{board}/snapshotsEvery complete leaderboard
Bounties/v1/bounties/historyEvery bounty that was claimed or removed
Economy/v1/economy/historyA sample every 15 minutes
Server/v1/server/samplesA check every minute

Every history endpoint takes the same three parameters on top of its own filters:

ParameterMeaning
fromOnly entries at or after this time, in Unix milliseconds.
toOnly entries before this time, in Unix milliseconds.
cursorThe next value of the previous page. See cursors.

Event feeds such as closed listings and fills come newest first, so the first page is what just happened. Time series such as prices, economy and server samples come oldest first, so they are ready to plot.

The last two listings that left the auction house:

curl "https://api.donutstats.io/v1/auction/history?limit=2" \
  -H "x-api-key: $DONUTSTATS_API_KEY"
Response200
{
  "limit": 2,
  "next": "MTc5MjcyNzYzMzAxOC4yMjAxNzk1OA",
  "entries": [
    {
      "id": 22026907,
      "product": {
        "key": "iron_bars",
        "item": "iron_bars",
        "name": "Iron Bars",
        "variant": "",
        "category": "blocks",
        "details": null
      },
      "count": 1,
      "price": 13900,
      "unitPrice": 13900,
      "details": null,
      "firstSeenAt": 1792726965330,
      "lastSeenAt": 1792726965330,
      "closedAt": 1792727633018,
      "seller": null
    },
    {
      "id": 22017958,
      "product": {
        "key": "iron_bars",
        "item": "iron_bars",
        "name": "Iron Bars",
        "variant": "",
        "category": "blocks",
        "details": null
      },
      "count": 64,
      "price": 19900,
      "unitPrice": 310.9375,
      "details": null,
      "firstSeenAt": 1792726638062,
      "lastSeenAt": 1792726965330,
      "closedAt": 1792727633018,
      "seller": null
    }
  ]
}

Daily diamond prices for one month:

curl "https://api.donutstats.io/v1/items/diamond/history?interval=day&from=1790049233018&to=1792727633018" \
  -H "x-api-key: $DONUTSTATS_API_KEY"

The auction house never says whether a listing sold or was taken down, but buy orders do: every Fill is items actually handed over and money actually paid. That makes fills the best measure of real trading. This adds up a week of bone sales per day:

const since = Date.now() - 7 * 86_400_000;
const totals = new Map();
let cursor = null;

do {
  const url = new URL("https://api.donutstats.io/v1/orders/fills");
  url.searchParams.set("item", "bone");
  url.searchParams.set("from", String(since));
  url.searchParams.set("limit", "1000");
  if (cursor) url.searchParams.set("cursor", cursor);
  const response = await fetch(url, { headers: { "x-api-key": process.env.DONUTSTATS_API_KEY } });
  if (!response.ok) throw new Error(`DonutStats API returned ${response.status}`);
  const page = await response.json();
  for (const fill of page.entries) {
    const day = new Date(fill.at).toISOString().slice(0, 10);
    const total = totals.get(day) ?? { quantity: 0, paid: 0 };
    total.quantity += fill.quantity;
    total.paid += fill.paid;
    totals.set(day, total);
  }
  cursor = page.next;
} while (cursor);

for (const [day, { quantity, paid }] of totals) {
  console.log(day, quantity, "bones sold for", Math.round(paid / quantity), "each");
}

Closed listings are likely sales

A listing that disappears within minutes of going up was almost certainly bought. One that sat for days and then vanished probably expired. Compare firstSeenAt and closedAt before you count it as a sale.

Each DonutSMP leaderboard is recorded in full every 6 hours. List the snapshots of a board, pick the one you want by finishedAt, then page through it like a normal leaderboard:

curl "https://api.donutstats.io/v1/leaderboards/money/snapshots?to=1791763200000&limit=1" \
  -H "x-api-key: $DONUTSTATS_API_KEY"

curl "https://api.donutstats.io/v1/leaderboards/money/snapshots/468?limit=500" \
  -H "x-api-key: $DONUTSTATS_API_KEY"

To follow one player instead of a whole board, use rank history. It returns their rank and value from every snapshot in one request.

  • Ask for the biggest page the endpoint allows. Most history fits in a few requests, which matters on the Free plan.
  • Store what you download and only ask for what is new next time: pass the newest time you have as from.
  • Gaps are real. If DonutSMP was down, there are no points for those hours. Plot them as gaps rather than joining the line. See gaps in history.