Skip to content
DonutStatsAPI
Sign up
Concepts

Pagination

Long lists come one page at a time. Live lists use page numbers and history uses cursors.

page starts at 1. limit is how many entries you get per page, up to a maximum that depends on the endpoint. Leave both out and you get the first page at the default size.

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

Paginated lists share one shape, the Page object. total counts every entry that matches your filters, so the number of pages is Math.ceil(total / limit).

Page
{
  "page": 1,
  "limit": 2,
  "total": 14033,
  "updatedAt": 1792724121936,
  "entries": [
    "one Order per entry"
  ]
}

Keep asking for the next page until you have seen total entries:

async function* everyPage(path, limit = 200) {
  for (let page = 1; ; page++) {
    const separator = path.includes("?") ? "&" : "?";
    const response = await fetch(
      `https://api.donutstats.io${path}${separator}page=${page}&limit=${limit}`,
      { headers: { "x-api-key": process.env.DONUTSTATS_API_KEY } },
    );
    if (!response.ok) throw new Error(`DonutStats API returned ${response.status}`);
    const body = await response.json();
    yield* body.entries;
    if (page * limit >= body.total) return;
  }
}

for await (const order of everyPage("/v1/orders?item=diamond")) {
  console.log(order.unitPrice, order.remaining);
}

Markets change while you page. A listing can sell between your requests and shift everything after it up by one, so the same entry can appear twice or be skipped. Two habits make that harmless:

  • Deduplicate by id. Listing and order ids never change.
  • Filter instead of reading everything. An item or category filter is faster and smaller.

Leaderboards barely move

Every leaderboard page comes from the latest complete snapshot, never a partial one. A new snapshot only takes over once it is finished, every 6 hours, so reading the whole baltop in one go almost always sees a single ranking. Compare board.updatedAt between pages if you need to be sure.

History endpoints can hold millions of entries, so they page with a cursor instead of page numbers. Every response is a Feed: entries, the limit you asked for, and next. Pass next back as cursor to get the following page. When next is null you have everything.

Feed
{
  "limit": 2,
  "next": "MTc5MjcyNzYzMzAxOC4yMjAxNzk1OA",
  "entries": [
    "one ClosedListing per entry"
  ]
}

Cursors have three useful properties:

  • Deep pages are as fast as the first one, however far back you go.
  • Nothing is skipped or repeated when new entries arrive while you page, because a cursor points at a position in time rather than a row number.
  • They combine with from and to, in Unix milliseconds, to read one window of time. Keep the same filters on every page.
OrderEndpoints
Newest firstAuction history, order history, order fills, bounty history and leaderboard snapshots.
Oldest firstPrice history, economy history and server samples. These are time series, so they read naturally from the start.

Treat cursors as opaque

A cursor is a short string. Store it if you want to resume later, but do not build or change one yourself. A malformed cursor is a 400.

Follow next until it runs out. With the biggest page size most history fits in a handful of requests:

async function* everyEntry(path) {
  let cursor = null;
  do {
    const url = new URL(`https://api.donutstats.io${path}`);
    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 body = await response.json();
    yield* body.entries;
    cursor = body.next;
  } while (cursor);
}

for await (const fill of everyEntry("/v1/orders/fills?item=bone&limit=1000")) {
  console.log(fill.at, fill.quantity, fill.paid);
}
EndpointPagingDefault limitMaximum
/v1/playersPage50200
/v1/players/{username}/listingsPage24100
/v1/leaderboards/{board}Page100500
/v1/leaderboards/{board}/snapshotsCursor100500
/v1/leaderboards/{board}/snapshots/{id}Page100500
/v1/auctionPage50200
/v1/auction/dealsPage24100
/v1/auction/historyCursor1001,000
/v1/ordersPage50200
/v1/orders/historyCursor1001,000
/v1/orders/fillsCursor1001,000
/v1/itemsPage50200
/v1/items/{key}/historyCursor10005,000
/v1/flipsPage50100
/v1/bountiesPage50200
/v1/bounties/historyCursor1001,000
/v1/economy/historyCursor10005,000
/v1/server/samplesCursor10005,000

Page numbers go up to 100,000. Cursors have no limit.