Developer API
Read-only JSON for everything DuckDuckRob shows publicly. No key, no signup, no writes.
Base URL https://ogn.tropixmc.dev/api/v1.
Quick start
Every endpoint is a plain GET that returns JSON. Try one now:
{
"key": "messages",
"label": "Messages",
"page": 1,
"limit": 3,
"total": 3612,
"pages": 1204,
"rows": [
{
"rank": 1,
"username": "DirectCube",
"value": 48213,
"display": "48,213",
"profileUrl": "https://example.com/profile/DirectCube",
"apiUrl": "https://example.com/api/v1/players/DirectCube",
"avatarUrl": "https://example.com/head/DirectCube?s=64"
}
]
}
Start at the index for a machine-readable list of every endpoint, or pull the OpenAPI 3.1 document into a client generator.
Conventions
- Versioning
- The version lives in the path (
/api/v1). Fields get added inside a version; nothing already documented is removed or renamed. Treat unknown fields as normal and ignore them. - Paging
- List endpoints take
?page=(1-based, default 1) and?limit=(default 20, max 100). Every list response carriespage,limit,totalandpagesalongside the rows. - Timestamps
- Milliseconds since the Unix epoch, as numbers.
nullmeans never recorded. Bucketed chart points use{ "t": <ms>, "v": <value> }. - Money and figures
- Numbers are raw, unformatted values. Where the site also prints a formatted string it ships next to the number (
valueanddisplay,moneyValueandmoney). A figure the server has never reported for that player isnull, not-1or0. - Rate limit
- 120 requests per 60 seconds per IP. Responses carry
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(epoch seconds); going over returns429withRetry-After. - Caching
- Successful responses set
Cache-Control: public, max-age=30. The live endpoints (/api/v1/health,/api/v1/online) sendno-store. - CORS
Access-Control-Allow-Origin: *, so browser code can call it directly. The API never reads your session cookie, so it returns the same body to everyone, logged in or not.- Maintenance
- While the site is locked for maintenance every endpoint answers
503with the error body below. Retry later.
Errors
Failures use the HTTP status plus one consistent body:
{ "error": { "code": "not_found", "message": "No tracked player by that name." } }
| Status | Code | Meaning |
|---|---|---|
| 400 | missing_query | A required query parameter was absent. |
| 400 | unknown_value | The trait value did not match any option. The message lists the valid ones. |
| 403 | section_hidden | The player switched that section off on their account page. |
| 403 | login_only | The player limits their profile to logged-in viewers, so it is never served over the API. |
| 404 | not_found | No such player, guild, warp or endpoint. |
| 404 | opted_out | The player opted out of the profiler entirely. |
| 404 | unknown_leaderboard, unknown_trait | Unrecognised key. The listing endpoint has the valid ones. |
| 405 | method_not_allowed | The API is read-only. Use GET. |
| 429 | rate_limited | Over the rate limit. Wait for Retry-After. |
| 503 | maintenance | The site is locked for maintenance. |
What the API will not show you
The API is held to the same rules as the pages. It never returns more than an anonymous visitor can already read on the site.
- A player who opted out is gone: absent from every list, and a direct lookup returns
404. An admin-hidden player is invisible entirely, as if the name were never tracked. - A player who limits their profile to logged-in viewers returns
403 login_only. There is no way to authenticate past this, by design. - Each profile section (activity, server figures, graphs, guild, friend map, misc, language, sample messages) is consent-gated. A player's response lists the sections they switched off in
hidden, and those keys come backnull; the dedicated sub-endpoints return403 section_hidden. /api/v1/onlinereports a count only. The roster is never exposed, so an opted-out player's presence cannot leak./api/v1/players/{name}/messagesreturns the same fixed sample the profile page shows, not a full chat log.- Members-only leaderboards are not listed and 404 on direct access.
- The joke stats (IQ, star sign, ideology and friends) are generated from the username. They are not facts about anyone.
Meta
Index of every endpoint, plus the rate limit in force.
OpenAPI 3.1 description of this API, for code generators and clients.
Liveness check with the current row counts.
Server
Site-wide totals: players, messages, donations, quotes, guilds, warps, crashes.
How many players are on the RPG server right now. Count only, never names.
Players
The player directory, busiest first.
| Parameter | In | Type | Description |
|---|---|---|---|
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
One player: identity, activity, server figures, guild, donations, badges, joke stats.
| Parameter | In | Type | Description |
|---|---|---|---|
name required |
path | string | Minecraft username, or any nickname the player has been seen under. |
Hour-of-day histogram, a message-count series, and the money/petals history.
| Parameter | In | Type | Description |
|---|---|---|---|
name required |
path | string | Minecraft username, or any nickname the player has been seen under. |
range |
query | string | Window for the message series and the history: 1d, 7d, 30d or all. Default all. |
Vocabulary metrics with top words, bigrams and emojis. Off unless the player opted in.
| Parameter | In | Type | Description |
|---|---|---|---|
name required |
path | string | Minecraft username, or any nickname the player has been seen under. |
A deterministic sample of the player's stored messages. Off unless the player opted in.
| Parameter | In | Type | Description |
|---|---|---|---|
name required |
path | string | Minecraft username, or any nickname the player has been seen under. |
Friend edges: who this player added, who added them, plus the computed best friend.
| Parameter | In | Type | Description |
|---|---|---|---|
name required |
path | string | Minecraft username, or any nickname the player has been seen under. |
Search
Substring search over usernames and seen nicknames.
| Parameter | In | Type | Description |
|---|---|---|---|
q required |
query | string | Search term, 1 to 64 characters. |
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
The filterable joke traits and every value each one can take.
Every player whose trait matches a value.
| Parameter | In | Type | Description |
|---|---|---|---|
field required |
path | string | Trait key from /traits, e.g. starsign. |
value required |
query | string | One of that trait's values. Matched case-insensitively; a unique substring works too. |
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
Leaderboards
Every public leaderboard and its key.
One leaderboard, ranked. Accepts the aliases the site and the Discord bot accept.
| Parameter | In | Type | Description |
|---|---|---|---|
stat required |
path | string | Board key from /leaderboards, e.g. messages. |
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
Community
Tracked guilds, strongest first.
| Parameter | In | Type | Description |
|---|---|---|---|
q |
query | string | Filter by name substring. |
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
One guild with its roster, allies and enemies.
| Parameter | In | Type | Description |
|---|---|---|---|
name required |
path | string | Guild name, case-insensitive. |
Player warps, most visited first.
| Parameter | In | Type | Description |
|---|---|---|---|
q |
query | string | Filter by warp name or owner substring. |
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
One player warp in full.
| Parameter | In | Type | Description |
|---|---|---|---|
name required |
path | string | Warp name, case-insensitive. |
Donation totals with the ranked donor board and the recent feed.
| Parameter | In | Type | Description |
|---|---|---|---|
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
The quotebook, newest first.
| Parameter | In | Type | Description |
|---|---|---|---|
page |
query | integer | 1-based page number. Default 1. |
limit |
query | integer | Rows per page. Default 20, max 100. |
Balance distribution: median, mean, Gini, top shares, magnitude histogram, richest players.
Response samples
GET /api/v1/players/{name}
Trimmed. hidden names the sections the player switched off; every key it lists is null in the body.
{
"username": "DirectCube",
"nicks": ["Cube"],
"resolved": true,
"staff": false,
"bio": null,
"timezone": "UTC",
"firstSeen": 1719792000000,
"lastSeen": 1753315200000,
"profileUrl": "https://example.com/profile/DirectCube",
"roles": { "admin": true, "verifiedOwner": true, "plus": true, "donorTier": "gold", "coolKid": false },
"donations": { "total": 250000000, "rank": 2, "tier": "gold" },
"hidden": ["summary", "samples"],
"activity": {
"messagesStored": 12043, "messagesLifetime": 48213, "welcomes": 311,
"avgLength": 27.4, "medianLength": 21, "avgWords": 5.2, "uniqueWords": 9218,
"questionPercent": 12, "links": 84, "busiestHour": 21, "busiestDay": "Saturday"
},
"server": {
"money": "$2,058,235,855.68", "moneyValue": 2058235855.68, "petals": 4120,
"class": "Mage", "playtimeHours": 913, "chatgameMisses": 7,
"claimBlocks": { "total": 182400, "fromPlaytime": 120000, "bonus": 62400, "spent": 40000, "left": 142400, "claims": 6 },
"warpsOwned": 2, "topOnePercent": true, "polledAt": 1753315000000
},
"guild": { "name": "Yuri", "level": 14, "members": 22, "membersMax": 30, "role": "owner", "rank": 1 },
"miscStats": [{ "label": "Star Sign", "value": "Leo" }],
"casino": { "balance": 15200.5, "bets": 431, "wagered": 98000, "profit": -4200.25 },
"badges": [{ "id": "admin", "label": "Admin", "description": "Site administrator" }]
}
GET /api/v1/players/{name}/activity
hours.counts is 24 entries, index 0 to 23 in the site timezone. messages.points fills quiet buckets with 0 so the series is never misdated.
{
"username": "DirectCube",
"range": "30d",
"timezone": "UTC",
"hours": { "counts": [12, 4, 1, 0, 0, 0, 2, 9, 40, 55, 61, 72, 80, 77, 69, 58, 63, 71, 88, 94, 101, 96, 60, 31], "total": 1144 },
"messages": { "unit": "day", "points": [{ "t": 1750723200000, "v": 41 }, { "t": 1750809600000, "v": 0 }] },
"history": { "money": [{ "t": 1750723200000, "v": 1900000000 }], "petals": [] },
"hidden": []
}
GET /api/v1/guilds/{name}
{
"name": "Yuri",
"description": "no rules",
"level": 14,
"members": 22,
"membersMax": 30,
"owner": "DirectCube",
"allies": ["Nova"],
"enemies": [],
"roster": [{ "username": "DirectCube", "role": "owner", "rank": 1 }],
"polledAt": 1753315000000
}
Using it well
- Cache. Responses are good for 30 seconds; polling faster only burns your rate limit.
- Page rather than scrape. Ask for the page you need instead of walking every player to build a local copy.
- Send a
User-Agentthat says who you are and how to reach you, so a problem gets an email instead of a block. - Honour
Retry-Afteron a429or503rather than retrying in a tight loop. - Do not republish a player's data in a way the site itself would not. The consent flags exist because players set them.
Questions, a raised limit, or a bug in a response: the community Discord. See also the Terms of Service and Privacy Policy.