Drop rates to a spreadsheet
Page through corroborated drop rates and save them as CSV.
Prerequisites#
- Node 18 or newer, or Python 3 with only its standard library. Nothing to install.
- A spreadsheet that opens CSV files: Excel, Google Sheets, LibreOffice or Numbers.
- No account. Without a key, this endpoint allows 30 requests an hour for each IP address and up to 500 rows a page. Limits and keys explains the free key that raises both.
Steps#
Read one page
A page of
GET /api/rollups/dropshas the rates, the credit line and anextlink. A rate is public once three installs looted its source. Trimmed, with placeholder values:One page{ "rates": [ { "sourceNpcId": 100, "itemId": 900, "kills": 40, "drops": 4, "rate": 0.1, "reporters": 3, "public": true } ], "measuredZeros": [], "offset": 0, "limit": 500, "total": 1, "totalRates": 1, "next": null, "attribution": { "credit": "Data from Everloot (https://everloot.app), CC BY-SA 4.0.", "…": "…" } }killscounts loot windows players opened on the source, not kills counted by the game. A rate has eithersourceNpcIdorsourceObjectId.rateis a fraction from 0 to 1.Follow next until it is null
Ask for 500 rows at a time. While
nextholds an address, fetch it. When it isnull, you have everything. The scripts do that, then write the CSV and a second file with the credit line.drops// Every corroborated drop rate, saved as drops.csv. Run: node drops.mjs import { writeFile } from "node:fs/promises"; const die = (message) => { console.error(message); process.exit(1); }; const BASE = "https://everloot.app"; let url = `${BASE}/api/rollups/drops?limit=500`; const rates = []; let credit = ""; while (url) { const res = await fetch(url); const page = await res.json(); if (res.status === 429) die(`Rate limited. Try again in ${res.headers.get("retry-after")} seconds.`); if (!res.ok) die(`${res.status}: ${page.error ?? "request failed"}`); if (page.unreachable) die("Storage could not be read; the empty list is not a zero. Try again later."); rates.push(...page.rates); credit = page.attribution.credit; url = page.next ? new URL(page.next, BASE).href : null; // null on the last page } const header = ["source_type", "source_id", "item_id", "kills", "drops", "rate", "reporters"]; const lines = rates.map((r) => [ r.sourceNpcId ? "npc" : "object", r.sourceNpcId ?? r.sourceObjectId, r.itemId, r.kills, r.drops, r.rate, r.reporters, ].join(",")); await writeFile("drops.csv", [header.join(","), ...lines].join("\n") + "\n"); await writeFile("drops-credit.txt", credit + "\n"); console.log(`Wrote ${rates.length} drop rates to drops.csv`); console.log(credit);
"""Every corroborated drop rate, saved as drops.csv. Run: python drops.py""" import csv import json import os from urllib.error import HTTPError from urllib.parse import urljoin from urllib.request import urlopen BASE = "https://everloot.app" url = BASE + "/api/rollups/drops?limit=500" rates = [] credit = "" while url: try: with urlopen(url, timeout=30) as res: page = json.load(res) except HTTPError as err: if err.code == 429: raise SystemExit(f"Rate limited. Try again in {err.headers['Retry-After']} seconds.") raise SystemExit(f"{err.code}: {json.load(err).get('error', 'request failed')}") if page["unreachable"]: raise SystemExit("Storage could not be read; the empty list is not a zero. Try again later.") rates.extend(page["rates"]) credit = page["attribution"]["credit"] url = urljoin(BASE, page["next"]) if page["next"] else None # None on the last page with open("drops.csv", "w", newline="", encoding="utf-8") as f: out = csv.writer(f) out.writerow(["source_type", "source_id", "item_id", "kills", "drops", "rate", "reporters"]) for r in rates: npc = r.get("sourceNpcId") out.writerow(["npc" if npc else "object", npc or r["sourceObjectId"], r["itemId"], r["kills"], r["drops"], r["rate"], r["reporters"]]) with open("drops-credit.txt", "w", encoding="utf-8") as f: f.write(credit + "\n") print(f"Wrote {len(rates)} drop rates to drops.csv") print(credit)
The same endpoint also lists
measuredZeros: sources players looted with nothing in their windows. The scripts leave them out. If the storage cannot be read, the answer says so, and the scripts stop instead of writing an empty file that looks like a result.Run it
Runnode drops.mjs
python drops.py
Today it prints this, and
drops.csvholds only the header row:OutputWrote 0 drop rates to drops.csv Data from Everloot (https://everloot.app), CC BY-SA 4.0.
source_type,source_id,item_id,kills,drops,rate,reporters
Once rates exist, each one is a row. Placeholder rows:
drops.csvsource_type,source_id,item_id,kills,drops,rate,reporters npc,100,900,40,4,0.1,3 npc,101,901,41,5,0.12195121951219512,3
Open it in a spreadsheet
Open
drops.csvin Excel, LibreOffice or Numbers, or in Google Sheets with File, Import, Upload. Format theratecolumn as a percentage. Then sort bykillsto see the best-measured rates first.Add the credit line
The data is CC BY-SA 4.0 and has to be credited. Open
drops-credit.txtand paste its line under the table, or into the sheet's header. It is theCREDIT_LINEfrom@everloot/protocol, and the endpoint sends it inattribution.creditand in theX-Everloot-Creditheader. If you publish the sheet or anything built from it, keep the line with it. Licence and credit has the full terms.
Check your work#
- The script prints how many rates it wrote and the credit line. The row count in the spreadsheet is that number plus the header row.
- Every
rateis between 0 and 1,. drops-credit.txtholds one line starting with "Data from Everloot".- A 429 stops the script with
Rate limited. Try again in N seconds.N is theRetry-Afterheader. Wait that long, or use a free key. - A file with only the header row is a real answer today. It means no source has three installs behind it yet, not that nothing drops.
You can also ask for a smaller page with ?offset=0&limit=100. A limit above your page size is capped, and next still leads on.
Next steps#
- /rollups/drops: every field.
- Data files: bulk downloads, if you want everything at once.
- Market prices in a script: the same idea for prices.