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.
Every history endpoint takes the same three parameters on top of its own filters:
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"{
"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.
