Skip to content

Repository files navigation

Doughmination API

A combined Discord presence (Lanyard-style) and profile/badges (dstn.to-style) API on a single Cloudflare Worker + Durable Objects, powered by one Discord bot. It also carries the Doughmination plural-system API (fronting, members, mental state, devices, guestbook), Minecraft lookups, and merged git contribution heatmaps. Everything returns one unified JSON envelope, and all live updates go over a single WebSocket.

Thanks

This code wasn't just me. It took a good chunk of my own brain plus a lot of help from Dustin (@dstn.to), who was really generous explaining how he handles the tricky parts: rate limits, caching, and getting Discord to actually trust your requests. Thanks Dustin! And credit to Phineas for Lanyard, which inspired the presence half of this.

Architecture

Two Durable Objects behind the Worker router (src/index.ts):

  • GATEWAY (GatewayManager) — holds the single Discord gateway socket, ingests presences from READY / GUILD_CREATE / PRESENCE_UPDATE, and keeps an in-memory userId → presence map. It doesn't serve browser sockets; it relays each live presence change to the SYSTEM DO for fan-out.
  • SYSTEM (SystemState) — all persistent state (users, tags, statuses, mental state, devices) in DO storage, the visit-counter SQLite tables, and the single realtime WebSocket hub.

A cron trigger (*/2 * * * *) keeps the gateway DO connected. Static assets under assets/ are served at the site root (/icon.png, /capes/*, …).

Endpoints

Full, filterable reference at /docs. The map:

NamespaceWhat it serves
WS /v2/wsThe one WebSocket for all live updates (see below)
/v2/lanyard/users, /v2/lanyard/users/:id, /v2/lanyard/statusREST presence (single, batch up to 100, gateway health)
/v2/discord/users/:id, /v2/discord/users?ids=…Full profile + badges + live presence, single or batch
/v2/discord/guilds/:invite, /v2/discord/girls/:idType/:idGuild info from an invite; role/member lookups
/v2/minecraft/general/:uuid, /v2/minecraft/capesMojang identity + skins/capes, vanilla cape catalogue
/v2/contribapiMerged git contribution heatmaps (GitHub + Codeberg)
/v2/plural/*, /v2/devices/*, /v2/guestbook/*, /v2/counter/*The plural-system API: fronting, members, mental state, devices/battery, guestbook, site visit counters
/v2/healthLiveness: gateway connection + DO reachability (200 ok / 503 degraded) — point uptime monitors here
/docs, /openapi.jsonFull HTML API reference + machine-readable OpenAPI 3.1 spec (both generated from src/apidata.ts)
/abuse, /terms, /privacy, /.well-known/security.txtAbuse reports, terms of service, privacy, vulnerability disclosure

All JSON responses share one envelope: { success, data } on success, { success: false, error: { code, message } } on failure.

Realtime — the single /v2/ws

There's exactly one socket (the old /v2/lanyard/ws and /v2/plural/ws are gone). Every frame is a { type, data } object.

On connect you get connection_established. These are then pushed to every client automatically as they happen:

  • fronters_update — who's fronting changed
  • mental_state_update — mental state changed
  • device_update — a device/battery report changed
  • force_refresh — admin asked all clients to refresh

Discord presence is opt-in (keeps traffic down). Send a subscribe frame:

{ "type": "subscribe", "all": true } // every tracked user
{ "type": "subscribe", "ids": ["123…", "456…"] } // just these users

You immediately get an init_state snapshot of the presences you asked for, then presence_update frames for those users only. Subscriptions persist across DO hibernation. Send the string ping to get pong.

Presence lives in the GATEWAY DO; when it changes, GATEWAY relays it to SYSTEM, which fans it out to the clients subscribed to that user.

Caching

See the notes in each source file; the short version:

DataWhereTTL
PresenceGATEWAY DO memorynever cached — live from the gateway
PluralKit data (system, members, fronters)SYSTEM DO memoryCACHE_TTL (default 30s), busted on any switch/member/tag/status change
Discord profilesKV (PROFILE_CACHE)PROFILE_CACHE_TTL_SECONDS (default 300s, min 60), jittered ±20%; rich (userbot) fetches back off on 429 via a shared cooldown key (30–300s)
Guild invitesKV300s
Guild membershipsKV6h
Client-mod badges (Equicord)KV1h, stale fallback
Minecraft generalKV5min; vanilla-cape registry kept permanently

HTTP Cache-Control: JSON API responses are no-store (never edge/browser cached). HTML pages (/docs, /abuse, /terms, /privacy), /openapi.json, and /v2/contribapi are public, max-age=3600. All responses carry security headers (HSTS, nosniff; CSP + frame-deny on HTML).

Setup

1. Discord application

  1. https://discord.com/developers/applicationsNew ApplicationBot.
  2. Reset Token, copy it (this is DISCORD_BOT_TOKEN).
  3. Under Privileged Gateway Intents, enable PRESENCE INTENT and SERVER MEMBERS INTENT.
  4. Invite the bot to a server that contains the people you want to track (OAuth2 URL generator → scope bot). Presence is only visible for users sharing a server with the bot — same model as Lanyard.
  5. Optionally set TRACKED_GUILD_IDS in wrangler.jsonc (comma-separated) to limit monitoring to specific servers; empty = every guild the bot can see.

2. Install & deploy

# REQUIRED
bun install
# KV namespace for profile cache — paste the printed id into wrangler.jsonc
bunx wrangler kv namespace create PROFILE_CACHE
# Secrets
bunx wrangler secret put DISCORD_BOT_TOKEN
# Optional, ToS risk — only if you want the rich badges:
bunx wrangler secret put DISCORD_USER_TOKEN
# Optional 2nd/3rd userbot:
bunx wrangler secret put DISCORD_USER_TOKEN2
bunx wrangler secret put DISCORD_USER_TOKEN3
# Local dev (uses .dev.vars — see .dev.vars.example)
bun dev
# Production
bun deploy

Other secrets for the plural-system half (JWT_SECRET, SYSTEM_TOKEN, TURNSTILE_SECRET, ADMIN_PASSWORD, DOUGH_BOT_TOKEN, BATTERY_API_KEYS) go in .dev.vars locally and wrangler secret put <NAME> in production — .dev.vars.example documents all of them.

3. Useful commands

bun typecheck # tsc --noEmit
bun tail # live production logs# Update X-Super-Properties when Discord bumps the client build:
bun decode "X-Super-Properties: [BASE64 HERE]"

Configuration (wrangler.jsonc vars)

VarPurpose
TRACKED_GUILD_IDSComma-separated guilds to monitor; empty = all the bot sees
MEMBERSHIP_GUILD_IDSGuilds to resolve per-user membership for; falls back to TRACKED_GUILD_IDS
PROFILE_CACHE_TTL_SECONDSProfile KV cache TTL (min 60)
CACHE_TTLPluralKit cache TTL in seconds
DISCORD_CLIENT_BUILD_NUMBERFor userbot X-Super-Properties (update with bun decode)
PRONOUNDB_API_BASE, TIMEZONE_API_BASE, REVIEWDB_API_BASEThird-party enrichment sources, overridable for self-hosted forks
BASE_URL, CORS_ORIGINSPlural-system base URL and CORS allowlist

Abuse, privacy & security

  • Terms:/terms — the short version: be reasonable with request volume or your IP gets blocked.
  • Privacy:/privacy — what's stored (guestbook entries, anonymous visit counts, brief Discord/Minecraft caches) and how to get it removed.
  • Report abuse / request data removal:/abuse or email abuse@doughmination.win.
  • Opt out of presence tracking: leave the Discord server(s) the bot is in, or email with your Discord user ID to be blocked from lookups.
  • Vulnerability disclosure: email the address above; machine-readable contact at /.well-known/security.txt. Responsible disclosure appreciated.

Licence

ESAL-2.1.

About

My personal API, which I dont mind people hooking into

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - doughmination/api: My personal API, which I dont mind people hooking into · GitHub
Skip to content

Repository files navigation

Doughmination API

A combined Discord presence (Lanyard-style) and profile/badges (dstn.to-style) API on a single Cloudflare Worker + Durable Objects, powered by one Discord bot. It also carries the Doughmination plural-system API (fronting, members, mental state, devices, guestbook), Minecraft lookups, and merged git contribution heatmaps. Everything returns one unified JSON envelope, and all live updates go over a single WebSocket.

Thanks

This code wasn't just me. It took a good chunk of my own brain plus a lot of help from Dustin (@dstn.to), who was really generous explaining how he handles the tricky parts: rate limits, caching, and getting Discord to actually trust your requests. Thanks Dustin! And credit to Phineas for Lanyard, which inspired the presence half of this.

Architecture

Two Durable Objects behind the Worker router (src/index.ts):

  • GATEWAY (GatewayManager) — holds the single Discord gateway socket, ingests presences from READY / GUILD_CREATE / PRESENCE_UPDATE, and keeps an in-memory userId → presence map. It doesn't serve browser sockets; it relays each live presence change to the SYSTEM DO for fan-out.
  • SYSTEM (SystemState) — all persistent state (users, tags, statuses, mental state, devices) in DO storage, the visit-counter SQLite tables, and the single realtime WebSocket hub.

A cron trigger (*/2 * * * *) keeps the gateway DO connected. Static assets under assets/ are served at the site root (/icon.png, /capes/*, …).

Endpoints

Full, filterable reference at /docs. The map:

NamespaceWhat it serves
WS /v2/wsThe one WebSocket for all live updates (see below)
/v2/lanyard/users, /v2/lanyard/users/:id, /v2/lanyard/statusREST presence (single, batch up to 100, gateway health)
/v2/discord/users/:id, /v2/discord/users?ids=…Full profile + badges + live presence, single or batch
/v2/discord/guilds/:invite, /v2/discord/girls/:idType/:idGuild info from an invite; role/member lookups
/v2/minecraft/general/:uuid, /v2/minecraft/capesMojang identity + skins/capes, vanilla cape catalogue
/v2/contribapiMerged git contribution heatmaps (GitHub + Codeberg)
/v2/plural/*, /v2/devices/*, /v2/guestbook/*, /v2/counter/*The plural-system API: fronting, members, mental state, devices/battery, guestbook, site visit counters
/v2/healthLiveness: gateway connection + DO reachability (200 ok / 503 degraded) — point uptime monitors here
/docs, /openapi.jsonFull HTML API reference + machine-readable OpenAPI 3.1 spec (both generated from src/apidata.ts)
/abuse, /terms, /privacy, /.well-known/security.txtAbuse reports, terms of service, privacy, vulnerability disclosure

All JSON responses share one envelope: { success, data } on success, { success: false, error: { code, message } } on failure.

Realtime — the single /v2/ws

There's exactly one socket (the old /v2/lanyard/ws and /v2/plural/ws are gone). Every frame is a { type, data } object.

On connect you get connection_established. These are then pushed to every client automatically as they happen:

  • fronters_update — who's fronting changed
  • mental_state_update — mental state changed
  • device_update — a device/battery report changed
  • force_refresh — admin asked all clients to refresh

Discord presence is opt-in (keeps traffic down). Send a subscribe frame:

{ "type": "subscribe", "all": true } // every tracked user
{ "type": "subscribe", "ids": ["123…", "456…"] } // just these users

You immediately get an init_state snapshot of the presences you asked for, then presence_update frames for those users only. Subscriptions persist across DO hibernation. Send the string ping to get pong.

Presence lives in the GATEWAY DO; when it changes, GATEWAY relays it to SYSTEM, which fans it out to the clients subscribed to that user.

Caching

See the notes in each source file; the short version:

DataWhereTTL
PresenceGATEWAY DO memorynever cached — live from the gateway
PluralKit data (system, members, fronters)SYSTEM DO memoryCACHE_TTL (default 30s), busted on any switch/member/tag/status change
Discord profilesKV (PROFILE_CACHE)PROFILE_CACHE_TTL_SECONDS (default 300s, min 60), jittered ±20%; rich (userbot) fetches back off on 429 via a shared cooldown key (30–300s)
Guild invitesKV300s
Guild membershipsKV6h
Client-mod badges (Equicord)KV1h, stale fallback
Minecraft generalKV5min; vanilla-cape registry kept permanently

HTTP Cache-Control: JSON API responses are no-store (never edge/browser cached). HTML pages (/docs, /abuse, /terms, /privacy), /openapi.json, and /v2/contribapi are public, max-age=3600. All responses carry security headers (HSTS, nosniff; CSP + frame-deny on HTML).

Setup

1. Discord application

  1. https://discord.com/developers/applicationsNew ApplicationBot.
  2. Reset Token, copy it (this is DISCORD_BOT_TOKEN).
  3. Under Privileged Gateway Intents, enable PRESENCE INTENT and SERVER MEMBERS INTENT.
  4. Invite the bot to a server that contains the people you want to track (OAuth2 URL generator → scope bot). Presence is only visible for users sharing a server with the bot — same model as Lanyard.
  5. Optionally set TRACKED_GUILD_IDS in wrangler.jsonc (comma-separated) to limit monitoring to specific servers; empty = every guild the bot can see.

2. Install & deploy

# REQUIRED
bun install
# KV namespace for profile cache — paste the printed id into wrangler.jsonc
bunx wrangler kv namespace create PROFILE_CACHE
# Secrets
bunx wrangler secret put DISCORD_BOT_TOKEN
# Optional, ToS risk — only if you want the rich badges:
bunx wrangler secret put DISCORD_USER_TOKEN
# Optional 2nd/3rd userbot:
bunx wrangler secret put DISCORD_USER_TOKEN2
bunx wrangler secret put DISCORD_USER_TOKEN3
# Local dev (uses .dev.vars — see .dev.vars.example)
bun dev
# Production
bun deploy

Other secrets for the plural-system half (JWT_SECRET, SYSTEM_TOKEN, TURNSTILE_SECRET, ADMIN_PASSWORD, DOUGH_BOT_TOKEN, BATTERY_API_KEYS) go in .dev.vars locally and wrangler secret put <NAME> in production — .dev.vars.example documents all of them.

3. Useful commands

bun typecheck # tsc --noEmit
bun tail # live production logs# Update X-Super-Properties when Discord bumps the client build:
bun decode "X-Super-Properties: [BASE64 HERE]"

Configuration (wrangler.jsonc vars)

VarPurpose
TRACKED_GUILD_IDSComma-separated guilds to monitor; empty = all the bot sees
MEMBERSHIP_GUILD_IDSGuilds to resolve per-user membership for; falls back to TRACKED_GUILD_IDS
PROFILE_CACHE_TTL_SECONDSProfile KV cache TTL (min 60)
CACHE_TTLPluralKit cache TTL in seconds
DISCORD_CLIENT_BUILD_NUMBERFor userbot X-Super-Properties (update with bun decode)
PRONOUNDB_API_BASE, TIMEZONE_API_BASE, REVIEWDB_API_BASEThird-party enrichment sources, overridable for self-hosted forks
BASE_URL, CORS_ORIGINSPlural-system base URL and CORS allowlist

Abuse, privacy & security

  • Terms:/terms — the short version: be reasonable with request volume or your IP gets blocked.
  • Privacy:/privacy — what's stored (guestbook entries, anonymous visit counts, brief Discord/Minecraft caches) and how to get it removed.
  • Report abuse / request data removal:/abuse or email abuse@doughmination.win.
  • Opt out of presence tracking: leave the Discord server(s) the bot is in, or email with your Discord user ID to be blocked from lookups.
  • Vulnerability disclosure: email the address above; machine-readable contact at /.well-known/security.txt. Responsible disclosure appreciated.

Licence

ESAL-2.1.

About

My personal API, which I dont mind people hooking into

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - doughmination/api: My personal API, which I dont mind people hooking into · GitHub
Skip to content

Repository files navigation

Doughmination API

A combined Discord presence (Lanyard-style) and profile/badges (dstn.to-style) API on a single Cloudflare Worker + Durable Objects, powered by one Discord bot. It also carries the Doughmination plural-system API (fronting, members, mental state, devices, guestbook), Minecraft lookups, and merged git contribution heatmaps. Everything returns one unified JSON envelope, and all live updates go over a single WebSocket.

Thanks

This code wasn't just me. It took a good chunk of my own brain plus a lot of help from Dustin (@dstn.to), who was really generous explaining how he handles the tricky parts: rate limits, caching, and getting Discord to actually trust your requests. Thanks Dustin! And credit to Phineas for Lanyard, which inspired the presence half of this.

Architecture

Two Durable Objects behind the Worker router (src/index.ts):

  • GATEWAY (GatewayManager) — holds the single Discord gateway socket, ingests presences from READY / GUILD_CREATE / PRESENCE_UPDATE, and keeps an in-memory userId → presence map. It doesn't serve browser sockets; it relays each live presence change to the SYSTEM DO for fan-out.
  • SYSTEM (SystemState) — all persistent state (users, tags, statuses, mental state, devices) in DO storage, the visit-counter SQLite tables, and the single realtime WebSocket hub.

A cron trigger (*/2 * * * *) keeps the gateway DO connected. Static assets under assets/ are served at the site root (/icon.png, /capes/*, …).

Endpoints

Full, filterable reference at /docs. The map:

NamespaceWhat it serves
WS /v2/wsThe one WebSocket for all live updates (see below)
/v2/lanyard/users, /v2/lanyard/users/:id, /v2/lanyard/statusREST presence (single, batch up to 100, gateway health)
/v2/discord/users/:id, /v2/discord/users?ids=…Full profile + badges + live presence, single or batch
/v2/discord/guilds/:invite, /v2/discord/girls/:idType/:idGuild info from an invite; role/member lookups
/v2/minecraft/general/:uuid, /v2/minecraft/capesMojang identity + skins/capes, vanilla cape catalogue
/v2/contribapiMerged git contribution heatmaps (GitHub + Codeberg)
/v2/plural/*, /v2/devices/*, /v2/guestbook/*, /v2/counter/*The plural-system API: fronting, members, mental state, devices/battery, guestbook, site visit counters
/v2/healthLiveness: gateway connection + DO reachability (200 ok / 503 degraded) — point uptime monitors here
/docs, /openapi.jsonFull HTML API reference + machine-readable OpenAPI 3.1 spec (both generated from src/apidata.ts)
/abuse, /terms, /privacy, /.well-known/security.txtAbuse reports, terms of service, privacy, vulnerability disclosure

All JSON responses share one envelope: { success, data } on success, { success: false, error: { code, message } } on failure.

Realtime — the single /v2/ws

There's exactly one socket (the old /v2/lanyard/ws and /v2/plural/ws are gone). Every frame is a { type, data } object.

On connect you get connection_established. These are then pushed to every client automatically as they happen:

  • fronters_update — who's fronting changed
  • mental_state_update — mental state changed
  • device_update — a device/battery report changed
  • force_refresh — admin asked all clients to refresh

Discord presence is opt-in (keeps traffic down). Send a subscribe frame:

{ "type": "subscribe", "all": true } // every tracked user
{ "type": "subscribe", "ids": ["123…", "456…"] } // just these users

You immediately get an init_state snapshot of the presences you asked for, then presence_update frames for those users only. Subscriptions persist across DO hibernation. Send the string ping to get pong.

Presence lives in the GATEWAY DO; when it changes, GATEWAY relays it to SYSTEM, which fans it out to the clients subscribed to that user.

Caching

See the notes in each source file; the short version:

DataWhereTTL
PresenceGATEWAY DO memorynever cached — live from the gateway
PluralKit data (system, members, fronters)SYSTEM DO memoryCACHE_TTL (default 30s), busted on any switch/member/tag/status change
Discord profilesKV (PROFILE_CACHE)PROFILE_CACHE_TTL_SECONDS (default 300s, min 60), jittered ±20%; rich (userbot) fetches back off on 429 via a shared cooldown key (30–300s)
Guild invitesKV300s
Guild membershipsKV6h
Client-mod badges (Equicord)KV1h, stale fallback
Minecraft generalKV5min; vanilla-cape registry kept permanently

HTTP Cache-Control: JSON API responses are no-store (never edge/browser cached). HTML pages (/docs, /abuse, /terms, /privacy), /openapi.json, and /v2/contribapi are public, max-age=3600. All responses carry security headers (HSTS, nosniff; CSP + frame-deny on HTML).

Setup

1. Discord application

  1. https://discord.com/developers/applicationsNew ApplicationBot.
  2. Reset Token, copy it (this is DISCORD_BOT_TOKEN).
  3. Under Privileged Gateway Intents, enable PRESENCE INTENT and SERVER MEMBERS INTENT.
  4. Invite the bot to a server that contains the people you want to track (OAuth2 URL generator → scope bot). Presence is only visible for users sharing a server with the bot — same model as Lanyard.
  5. Optionally set TRACKED_GUILD_IDS in wrangler.jsonc (comma-separated) to limit monitoring to specific servers; empty = every guild the bot can see.

2. Install & deploy

# REQUIRED
bun install
# KV namespace for profile cache — paste the printed id into wrangler.jsonc
bunx wrangler kv namespace create PROFILE_CACHE
# Secrets
bunx wrangler secret put DISCORD_BOT_TOKEN
# Optional, ToS risk — only if you want the rich badges:
bunx wrangler secret put DISCORD_USER_TOKEN
# Optional 2nd/3rd userbot:
bunx wrangler secret put DISCORD_USER_TOKEN2
bunx wrangler secret put DISCORD_USER_TOKEN3
# Local dev (uses .dev.vars — see .dev.vars.example)
bun dev
# Production
bun deploy

Other secrets for the plural-system half (JWT_SECRET, SYSTEM_TOKEN, TURNSTILE_SECRET, ADMIN_PASSWORD, DOUGH_BOT_TOKEN, BATTERY_API_KEYS) go in .dev.vars locally and wrangler secret put <NAME> in production — .dev.vars.example documents all of them.

3. Useful commands

bun typecheck # tsc --noEmit
bun tail # live production logs# Update X-Super-Properties when Discord bumps the client build:
bun decode "X-Super-Properties: [BASE64 HERE]"

Configuration (wrangler.jsonc vars)

VarPurpose
TRACKED_GUILD_IDSComma-separated guilds to monitor; empty = all the bot sees
MEMBERSHIP_GUILD_IDSGuilds to resolve per-user membership for; falls back to TRACKED_GUILD_IDS
PROFILE_CACHE_TTL_SECONDSProfile KV cache TTL (min 60)
CACHE_TTLPluralKit cache TTL in seconds
DISCORD_CLIENT_BUILD_NUMBERFor userbot X-Super-Properties (update with bun decode)
PRONOUNDB_API_BASE, TIMEZONE_API_BASE, REVIEWDB_API_BASEThird-party enrichment sources, overridable for self-hosted forks
BASE_URL, CORS_ORIGINSPlural-system base URL and CORS allowlist

Abuse, privacy & security

  • Terms:/terms — the short version: be reasonable with request volume or your IP gets blocked.
  • Privacy:/privacy — what's stored (guestbook entries, anonymous visit counts, brief Discord/Minecraft caches) and how to get it removed.
  • Report abuse / request data removal:/abuse or email abuse@doughmination.win.
  • Opt out of presence tracking: leave the Discord server(s) the bot is in, or email with your Discord user ID to be blocked from lookups.
  • Vulnerability disclosure: email the address above; machine-readable contact at /.well-known/security.txt. Responsible disclosure appreciated.

Licence

ESAL-2.1.

About

My personal API, which I dont mind people hooking into

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - doughmination/api: My personal API, which I dont mind people hooking into · GitHub
Skip to content

Repository files navigation

Doughmination API

A combined Discord presence (Lanyard-style) and profile/badges (dstn.to-style) API on a single Cloudflare Worker + Durable Objects, powered by one Discord bot. It also carries the Doughmination plural-system API (fronting, members, mental state, devices, guestbook), Minecraft lookups, and merged git contribution heatmaps. Everything returns one unified JSON envelope, and all live updates go over a single WebSocket.

Thanks

This code wasn't just me. It took a good chunk of my own brain plus a lot of help from Dustin (@dstn.to), who was really generous explaining how he handles the tricky parts: rate limits, caching, and getting Discord to actually trust your requests. Thanks Dustin! And credit to Phineas for Lanyard, which inspired the presence half of this.

Architecture

Two Durable Objects behind the Worker router (src/index.ts):

  • GATEWAY (GatewayManager) — holds the single Discord gateway socket, ingests presences from READY / GUILD_CREATE / PRESENCE_UPDATE, and keeps an in-memory userId → presence map. It doesn't serve browser sockets; it relays each live presence change to the SYSTEM DO for fan-out.
  • SYSTEM (SystemState) — all persistent state (users, tags, statuses, mental state, devices) in DO storage, the visit-counter SQLite tables, and the single realtime WebSocket hub.

A cron trigger (*/2 * * * *) keeps the gateway DO connected. Static assets under assets/ are served at the site root (/icon.png, /capes/*, …).

Endpoints

Full, filterable reference at /docs. The map:

NamespaceWhat it serves
WS /v2/wsThe one WebSocket for all live updates (see below)
/v2/lanyard/users, /v2/lanyard/users/:id, /v2/lanyard/statusREST presence (single, batch up to 100, gateway health)
/v2/discord/users/:id, /v2/discord/users?ids=…Full profile + badges + live presence, single or batch
/v2/discord/guilds/:invite, /v2/discord/girls/:idType/:idGuild info from an invite; role/member lookups
/v2/minecraft/general/:uuid, /v2/minecraft/capesMojang identity + skins/capes, vanilla cape catalogue
/v2/contribapiMerged git contribution heatmaps (GitHub + Codeberg)
/v2/plural/*, /v2/devices/*, /v2/guestbook/*, /v2/counter/*The plural-system API: fronting, members, mental state, devices/battery, guestbook, site visit counters
/v2/healthLiveness: gateway connection + DO reachability (200 ok / 503 degraded) — point uptime monitors here
/docs, /openapi.jsonFull HTML API reference + machine-readable OpenAPI 3.1 spec (both generated from src/apidata.ts)
/abuse, /terms, /privacy, /.well-known/security.txtAbuse reports, terms of service, privacy, vulnerability disclosure

All JSON responses share one envelope: { success, data } on success, { success: false, error: { code, message } } on failure.

Realtime — the single /v2/ws

There's exactly one socket (the old /v2/lanyard/ws and /v2/plural/ws are gone). Every frame is a { type, data } object.

On connect you get connection_established. These are then pushed to every client automatically as they happen:

  • fronters_update — who's fronting changed
  • mental_state_update — mental state changed
  • device_update — a device/battery report changed
  • force_refresh — admin asked all clients to refresh

Discord presence is opt-in (keeps traffic down). Send a subscribe frame:

{ "type": "subscribe", "all": true } // every tracked user
{ "type": "subscribe", "ids": ["123…", "456…"] } // just these users

You immediately get an init_state snapshot of the presences you asked for, then presence_update frames for those users only. Subscriptions persist across DO hibernation. Send the string ping to get pong.

Presence lives in the GATEWAY DO; when it changes, GATEWAY relays it to SYSTEM, which fans it out to the clients subscribed to that user.

Caching

See the notes in each source file; the short version:

DataWhereTTL
PresenceGATEWAY DO memorynever cached — live from the gateway
PluralKit data (system, members, fronters)SYSTEM DO memoryCACHE_TTL (default 30s), busted on any switch/member/tag/status change
Discord profilesKV (PROFILE_CACHE)PROFILE_CACHE_TTL_SECONDS (default 300s, min 60), jittered ±20%; rich (userbot) fetches back off on 429 via a shared cooldown key (30–300s)
Guild invitesKV300s
Guild membershipsKV6h
Client-mod badges (Equicord)KV1h, stale fallback
Minecraft generalKV5min; vanilla-cape registry kept permanently

HTTP Cache-Control: JSON API responses are no-store (never edge/browser cached). HTML pages (/docs, /abuse, /terms, /privacy), /openapi.json, and /v2/contribapi are public, max-age=3600. All responses carry security headers (HSTS, nosniff; CSP + frame-deny on HTML).

Setup

1. Discord application

  1. https://discord.com/developers/applicationsNew ApplicationBot.
  2. Reset Token, copy it (this is DISCORD_BOT_TOKEN).
  3. Under Privileged Gateway Intents, enable PRESENCE INTENT and SERVER MEMBERS INTENT.
  4. Invite the bot to a server that contains the people you want to track (OAuth2 URL generator → scope bot). Presence is only visible for users sharing a server with the bot — same model as Lanyard.
  5. Optionally set TRACKED_GUILD_IDS in wrangler.jsonc (comma-separated) to limit monitoring to specific servers; empty = every guild the bot can see.

2. Install & deploy

# REQUIRED
bun install
# KV namespace for profile cache — paste the printed id into wrangler.jsonc
bunx wrangler kv namespace create PROFILE_CACHE
# Secrets
bunx wrangler secret put DISCORD_BOT_TOKEN
# Optional, ToS risk — only if you want the rich badges:
bunx wrangler secret put DISCORD_USER_TOKEN
# Optional 2nd/3rd userbot:
bunx wrangler secret put DISCORD_USER_TOKEN2
bunx wrangler secret put DISCORD_USER_TOKEN3
# Local dev (uses .dev.vars — see .dev.vars.example)
bun dev
# Production
bun deploy

Other secrets for the plural-system half (JWT_SECRET, SYSTEM_TOKEN, TURNSTILE_SECRET, ADMIN_PASSWORD, DOUGH_BOT_TOKEN, BATTERY_API_KEYS) go in .dev.vars locally and wrangler secret put <NAME> in production — .dev.vars.example documents all of them.

3. Useful commands

bun typecheck # tsc --noEmit
bun tail # live production logs# Update X-Super-Properties when Discord bumps the client build:
bun decode "X-Super-Properties: [BASE64 HERE]"

Configuration (wrangler.jsonc vars)

VarPurpose
TRACKED_GUILD_IDSComma-separated guilds to monitor; empty = all the bot sees
MEMBERSHIP_GUILD_IDSGuilds to resolve per-user membership for; falls back to TRACKED_GUILD_IDS
PROFILE_CACHE_TTL_SECONDSProfile KV cache TTL (min 60)
CACHE_TTLPluralKit cache TTL in seconds
DISCORD_CLIENT_BUILD_NUMBERFor userbot X-Super-Properties (update with bun decode)
PRONOUNDB_API_BASE, TIMEZONE_API_BASE, REVIEWDB_API_BASEThird-party enrichment sources, overridable for self-hosted forks
BASE_URL, CORS_ORIGINSPlural-system base URL and CORS allowlist

Abuse, privacy & security

  • Terms:/terms — the short version: be reasonable with request volume or your IP gets blocked.
  • Privacy:/privacy — what's stored (guestbook entries, anonymous visit counts, brief Discord/Minecraft caches) and how to get it removed.
  • Report abuse / request data removal:/abuse or email abuse@doughmination.win.
  • Opt out of presence tracking: leave the Discord server(s) the bot is in, or email with your Discord user ID to be blocked from lookups.
  • Vulnerability disclosure: email the address above; machine-readable contact at /.well-known/security.txt. Responsible disclosure appreciated.

Licence

ESAL-2.1.

About

My personal API, which I dont mind people hooking into

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - doughmination/api: My personal API, which I dont mind people hooking into · GitHub
Skip to content

Repository files navigation

Doughmination API

A combined Discord presence (Lanyard-style) and profile/badges (dstn.to-style) API on a single Cloudflare Worker + Durable Objects, powered by one Discord bot. It also carries the Doughmination plural-system API (fronting, members, mental state, devices, guestbook), Minecraft lookups, and merged git contribution heatmaps. Everything returns one unified JSON envelope, and all live updates go over a single WebSocket.

Thanks

This code wasn't just me. It took a good chunk of my own brain plus a lot of help from Dustin (@dstn.to), who was really generous explaining how he handles the tricky parts: rate limits, caching, and getting Discord to actually trust your requests. Thanks Dustin! And credit to Phineas for Lanyard, which inspired the presence half of this.

Architecture

Two Durable Objects behind the Worker router (src/index.ts):

  • GATEWAY (GatewayManager) — holds the single Discord gateway socket, ingests presences from READY / GUILD_CREATE / PRESENCE_UPDATE, and keeps an in-memory userId → presence map. It doesn't serve browser sockets; it relays each live presence change to the SYSTEM DO for fan-out.
  • SYSTEM (SystemState) — all persistent state (users, tags, statuses, mental state, devices) in DO storage, the visit-counter SQLite tables, and the single realtime WebSocket hub.

A cron trigger (*/2 * * * *) keeps the gateway DO connected. Static assets under assets/ are served at the site root (/icon.png, /capes/*, …).

Endpoints

Full, filterable reference at /docs. The map:

NamespaceWhat it serves
WS /v2/wsThe one WebSocket for all live updates (see below)
/v2/lanyard/users, /v2/lanyard/users/:id, /v2/lanyard/statusREST presence (single, batch up to 100, gateway health)
/v2/discord/users/:id, /v2/discord/users?ids=…Full profile + badges + live presence, single or batch
/v2/discord/guilds/:invite, /v2/discord/girls/:idType/:idGuild info from an invite; role/member lookups
/v2/minecraft/general/:uuid, /v2/minecraft/capesMojang identity + skins/capes, vanilla cape catalogue
/v2/contribapiMerged git contribution heatmaps (GitHub + Codeberg)
/v2/plural/*, /v2/devices/*, /v2/guestbook/*, /v2/counter/*The plural-system API: fronting, members, mental state, devices/battery, guestbook, site visit counters
/v2/healthLiveness: gateway connection + DO reachability (200 ok / 503 degraded) — point uptime monitors here
/docs, /openapi.jsonFull HTML API reference + machine-readable OpenAPI 3.1 spec (both generated from src/apidata.ts)
/abuse, /terms, /privacy, /.well-known/security.txtAbuse reports, terms of service, privacy, vulnerability disclosure

All JSON responses share one envelope: { success, data } on success, { success: false, error: { code, message } } on failure.

Realtime — the single /v2/ws

There's exactly one socket (the old /v2/lanyard/ws and /v2/plural/ws are gone). Every frame is a { type, data } object.

On connect you get connection_established. These are then pushed to every client automatically as they happen:

  • fronters_update — who's fronting changed
  • mental_state_update — mental state changed
  • device_update — a device/battery report changed
  • force_refresh — admin asked all clients to refresh

Discord presence is opt-in (keeps traffic down). Send a subscribe frame:

{ "type": "subscribe", "all": true } // every tracked user
{ "type": "subscribe", "ids": ["123…", "456…"] } // just these users

You immediately get an init_state snapshot of the presences you asked for, then presence_update frames for those users only. Subscriptions persist across DO hibernation. Send the string ping to get pong.

Presence lives in the GATEWAY DO; when it changes, GATEWAY relays it to SYSTEM, which fans it out to the clients subscribed to that user.

Caching

See the notes in each source file; the short version:

DataWhereTTL
PresenceGATEWAY DO memorynever cached — live from the gateway
PluralKit data (system, members, fronters)SYSTEM DO memoryCACHE_TTL (default 30s), busted on any switch/member/tag/status change
Discord profilesKV (PROFILE_CACHE)PROFILE_CACHE_TTL_SECONDS (default 300s, min 60), jittered ±20%; rich (userbot) fetches back off on 429 via a shared cooldown key (30–300s)
Guild invitesKV300s
Guild membershipsKV6h
Client-mod badges (Equicord)KV1h, stale fallback
Minecraft generalKV5min; vanilla-cape registry kept permanently

HTTP Cache-Control: JSON API responses are no-store (never edge/browser cached). HTML pages (/docs, /abuse, /terms, /privacy), /openapi.json, and /v2/contribapi are public, max-age=3600. All responses carry security headers (HSTS, nosniff; CSP + frame-deny on HTML).

Setup

1. Discord application

  1. https://discord.com/developers/applicationsNew ApplicationBot.
  2. Reset Token, copy it (this is DISCORD_BOT_TOKEN).
  3. Under Privileged Gateway Intents, enable PRESENCE INTENT and SERVER MEMBERS INTENT.
  4. Invite the bot to a server that contains the people you want to track (OAuth2 URL generator → scope bot). Presence is only visible for users sharing a server with the bot — same model as Lanyard.
  5. Optionally set TRACKED_GUILD_IDS in wrangler.jsonc (comma-separated) to limit monitoring to specific servers; empty = every guild the bot can see.

2. Install & deploy

# REQUIRED
bun install
# KV namespace for profile cache — paste the printed id into wrangler.jsonc
bunx wrangler kv namespace create PROFILE_CACHE
# Secrets
bunx wrangler secret put DISCORD_BOT_TOKEN
# Optional, ToS risk — only if you want the rich badges:
bunx wrangler secret put DISCORD_USER_TOKEN
# Optional 2nd/3rd userbot:
bunx wrangler secret put DISCORD_USER_TOKEN2
bunx wrangler secret put DISCORD_USER_TOKEN3
# Local dev (uses .dev.vars — see .dev.vars.example)
bun dev
# Production
bun deploy

Other secrets for the plural-system half (JWT_SECRET, SYSTEM_TOKEN, TURNSTILE_SECRET, ADMIN_PASSWORD, DOUGH_BOT_TOKEN, BATTERY_API_KEYS) go in .dev.vars locally and wrangler secret put <NAME> in production — .dev.vars.example documents all of them.

3. Useful commands

bun typecheck # tsc --noEmit
bun tail # live production logs# Update X-Super-Properties when Discord bumps the client build:
bun decode "X-Super-Properties: [BASE64 HERE]"

Configuration (wrangler.jsonc vars)

VarPurpose
TRACKED_GUILD_IDSComma-separated guilds to monitor; empty = all the bot sees
MEMBERSHIP_GUILD_IDSGuilds to resolve per-user membership for; falls back to TRACKED_GUILD_IDS
PROFILE_CACHE_TTL_SECONDSProfile KV cache TTL (min 60)
CACHE_TTLPluralKit cache TTL in seconds
DISCORD_CLIENT_BUILD_NUMBERFor userbot X-Super-Properties (update with bun decode)
PRONOUNDB_API_BASE, TIMEZONE_API_BASE, REVIEWDB_API_BASEThird-party enrichment sources, overridable for self-hosted forks
BASE_URL, CORS_ORIGINSPlural-system base URL and CORS allowlist

Abuse, privacy & security

  • Terms:/terms — the short version: be reasonable with request volume or your IP gets blocked.
  • Privacy:/privacy — what's stored (guestbook entries, anonymous visit counts, brief Discord/Minecraft caches) and how to get it removed.
  • Report abuse / request data removal:/abuse or email abuse@doughmination.win.
  • Opt out of presence tracking: leave the Discord server(s) the bot is in, or email with your Discord user ID to be blocked from lookups.
  • Vulnerability disclosure: email the address above; machine-readable contact at /.well-known/security.txt. Responsible disclosure appreciated.

Licence

ESAL-2.1.

About

My personal API, which I dont mind people hooking into

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - doughmination/api: My personal API, which I dont mind people hooking into · GitHub
Skip to content

Repository files navigation

Doughmination API

A combined Discord presence (Lanyard-style) and profile/badges (dstn.to-style) API on a single Cloudflare Worker + Durable Objects, powered by one Discord bot. It also carries the Doughmination plural-system API (fronting, members, mental state, devices, guestbook), Minecraft lookups, and merged git contribution heatmaps. Everything returns one unified JSON envelope, and all live updates go over a single WebSocket.

Thanks

This code wasn't just me. It took a good chunk of my own brain plus a lot of help from Dustin (@dstn.to), who was really generous explaining how he handles the tricky parts: rate limits, caching, and getting Discord to actually trust your requests. Thanks Dustin! And credit to Phineas for Lanyard, which inspired the presence half of this.

Architecture

Two Durable Objects behind the Worker router (src/index.ts):

  • GATEWAY (GatewayManager) — holds the single Discord gateway socket, ingests presences from READY / GUILD_CREATE / PRESENCE_UPDATE, and keeps an in-memory userId → presence map. It doesn't serve browser sockets; it relays each live presence change to the SYSTEM DO for fan-out.
  • SYSTEM (SystemState) — all persistent state (users, tags, statuses, mental state, devices) in DO storage, the visit-counter SQLite tables, and the single realtime WebSocket hub.

A cron trigger (*/2 * * * *) keeps the gateway DO connected. Static assets under assets/ are served at the site root (/icon.png, /capes/*, …).

Endpoints

Full, filterable reference at /docs. The map:

NamespaceWhat it serves
WS /v2/wsThe one WebSocket for all live updates (see below)
/v2/lanyard/users, /v2/lanyard/users/:id, /v2/lanyard/statusREST presence (single, batch up to 100, gateway health)
/v2/discord/users/:id, /v2/discord/users?ids=…Full profile + badges + live presence, single or batch
/v2/discord/guilds/:invite, /v2/discord/girls/:idType/:idGuild info from an invite; role/member lookups
/v2/minecraft/general/:uuid, /v2/minecraft/capesMojang identity + skins/capes, vanilla cape catalogue
/v2/contribapiMerged git contribution heatmaps (GitHub + Codeberg)
/v2/plural/*, /v2/devices/*, /v2/guestbook/*, /v2/counter/*The plural-system API: fronting, members, mental state, devices/battery, guestbook, site visit counters
/v2/healthLiveness: gateway connection + DO reachability (200 ok / 503 degraded) — point uptime monitors here
/docs, /openapi.jsonFull HTML API reference + machine-readable OpenAPI 3.1 spec (both generated from src/apidata.ts)
/abuse, /terms, /privacy, /.well-known/security.txtAbuse reports, terms of service, privacy, vulnerability disclosure

All JSON responses share one envelope: { success, data } on success, { success: false, error: { code, message } } on failure.

Realtime — the single /v2/ws

There's exactly one socket (the old /v2/lanyard/ws and /v2/plural/ws are gone). Every frame is a { type, data } object.

On connect you get connection_established. These are then pushed to every client automatically as they happen:

  • fronters_update — who's fronting changed
  • mental_state_update — mental state changed
  • device_update — a device/battery report changed
  • force_refresh — admin asked all clients to refresh

Discord presence is opt-in (keeps traffic down). Send a subscribe frame:

{ "type": "subscribe", "all": true } // every tracked user
{ "type": "subscribe", "ids": ["123…", "456…"] } // just these users

You immediately get an init_state snapshot of the presences you asked for, then presence_update frames for those users only. Subscriptions persist across DO hibernation. Send the string ping to get pong.

Presence lives in the GATEWAY DO; when it changes, GATEWAY relays it to SYSTEM, which fans it out to the clients subscribed to that user.

Caching

See the notes in each source file; the short version:

DataWhereTTL
PresenceGATEWAY DO memorynever cached — live from the gateway
PluralKit data (system, members, fronters)SYSTEM DO memoryCACHE_TTL (default 30s), busted on any switch/member/tag/status change
Discord profilesKV (PROFILE_CACHE)PROFILE_CACHE_TTL_SECONDS (default 300s, min 60), jittered ±20%; rich (userbot) fetches back off on 429 via a shared cooldown key (30–300s)
Guild invitesKV300s
Guild membershipsKV6h
Client-mod badges (Equicord)KV1h, stale fallback
Minecraft generalKV5min; vanilla-cape registry kept permanently

HTTP Cache-Control: JSON API responses are no-store (never edge/browser cached). HTML pages (/docs, /abuse, /terms, /privacy), /openapi.json, and /v2/contribapi are public, max-age=3600. All responses carry security headers (HSTS, nosniff; CSP + frame-deny on HTML).

Setup

1. Discord application

  1. https://discord.com/developers/applicationsNew ApplicationBot.
  2. Reset Token, copy it (this is DISCORD_BOT_TOKEN).
  3. Under Privileged Gateway Intents, enable PRESENCE INTENT and SERVER MEMBERS INTENT.
  4. Invite the bot to a server that contains the people you want to track (OAuth2 URL generator → scope bot). Presence is only visible for users sharing a server with the bot — same model as Lanyard.
  5. Optionally set TRACKED_GUILD_IDS in wrangler.jsonc (comma-separated) to limit monitoring to specific servers; empty = every guild the bot can see.

2. Install & deploy

# REQUIRED
bun install
# KV namespace for profile cache — paste the printed id into wrangler.jsonc
bunx wrangler kv namespace create PROFILE_CACHE
# Secrets
bunx wrangler secret put DISCORD_BOT_TOKEN
# Optional, ToS risk — only if you want the rich badges:
bunx wrangler secret put DISCORD_USER_TOKEN
# Optional 2nd/3rd userbot:
bunx wrangler secret put DISCORD_USER_TOKEN2
bunx wrangler secret put DISCORD_USER_TOKEN3
# Local dev (uses .dev.vars — see .dev.vars.example)
bun dev
# Production
bun deploy

Other secrets for the plural-system half (JWT_SECRET, SYSTEM_TOKEN, TURNSTILE_SECRET, ADMIN_PASSWORD, DOUGH_BOT_TOKEN, BATTERY_API_KEYS) go in .dev.vars locally and wrangler secret put <NAME> in production — .dev.vars.example documents all of them.

3. Useful commands

bun typecheck # tsc --noEmit
bun tail # live production logs# Update X-Super-Properties when Discord bumps the client build:
bun decode "X-Super-Properties: [BASE64 HERE]"

Configuration (wrangler.jsonc vars)

VarPurpose
TRACKED_GUILD_IDSComma-separated guilds to monitor; empty = all the bot sees
MEMBERSHIP_GUILD_IDSGuilds to resolve per-user membership for; falls back to TRACKED_GUILD_IDS
PROFILE_CACHE_TTL_SECONDSProfile KV cache TTL (min 60)
CACHE_TTLPluralKit cache TTL in seconds
DISCORD_CLIENT_BUILD_NUMBERFor userbot X-Super-Properties (update with bun decode)
PRONOUNDB_API_BASE, TIMEZONE_API_BASE, REVIEWDB_API_BASEThird-party enrichment sources, overridable for self-hosted forks
BASE_URL, CORS_ORIGINSPlural-system base URL and CORS allowlist

Abuse, privacy & security

  • Terms:/terms — the short version: be reasonable with request volume or your IP gets blocked.
  • Privacy:/privacy — what's stored (guestbook entries, anonymous visit counts, brief Discord/Minecraft caches) and how to get it removed.
  • Report abuse / request data removal:/abuse or email abuse@doughmination.win.
  • Opt out of presence tracking: leave the Discord server(s) the bot is in, or email with your Discord user ID to be blocked from lookups.
  • Vulnerability disclosure: email the address above; machine-readable contact at /.well-known/security.txt. Responsible disclosure appreciated.

Licence

ESAL-2.1.

About

My personal API, which I dont mind people hooking into

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - doughmination/api: My personal API, which I dont mind people hooking into · GitHub
Skip to content

Repository files navigation

Doughmination API

A combined Discord presence (Lanyard-style) and profile/badges (dstn.to-style) API on a single Cloudflare Worker + Durable Objects, powered by one Discord bot. It also carries the Doughmination plural-system API (fronting, members, mental state, devices, guestbook), Minecraft lookups, and merged git contribution heatmaps. Everything returns one unified JSON envelope, and all live updates go over a single WebSocket.

Thanks

This code wasn't just me. It took a good chunk of my own brain plus a lot of help from Dustin (@dstn.to), who was really generous explaining how he handles the tricky parts: rate limits, caching, and getting Discord to actually trust your requests. Thanks Dustin! And credit to Phineas for Lanyard, which inspired the presence half of this.

Architecture

Two Durable Objects behind the Worker router (src/index.ts):

  • GATEWAY (GatewayManager) — holds the single Discord gateway socket, ingests presences from READY / GUILD_CREATE / PRESENCE_UPDATE, and keeps an in-memory userId → presence map. It doesn't serve browser sockets; it relays each live presence change to the SYSTEM DO for fan-out.
  • SYSTEM (SystemState) — all persistent state (users, tags, statuses, mental state, devices) in DO storage, the visit-counter SQLite tables, and the single realtime WebSocket hub.

A cron trigger (*/2 * * * *) keeps the gateway DO connected. Static assets under assets/ are served at the site root (/icon.png, /capes/*, …).

Endpoints

Full, filterable reference at /docs. The map:

NamespaceWhat it serves
WS /v2/wsThe one WebSocket for all live updates (see below)
/v2/lanyard/users, /v2/lanyard/users/:id, /v2/lanyard/statusREST presence (single, batch up to 100, gateway health)
/v2/discord/users/:id, /v2/discord/users?ids=…Full profile + badges + live presence, single or batch
/v2/discord/guilds/:invite, /v2/discord/girls/:idType/:idGuild info from an invite; role/member lookups
/v2/minecraft/general/:uuid, /v2/minecraft/capesMojang identity + skins/capes, vanilla cape catalogue
/v2/contribapiMerged git contribution heatmaps (GitHub + Codeberg)
/v2/plural/*, /v2/devices/*, /v2/guestbook/*, /v2/counter/*The plural-system API: fronting, members, mental state, devices/battery, guestbook, site visit counters
/v2/healthLiveness: gateway connection + DO reachability (200 ok / 503 degraded) — point uptime monitors here
/docs, /openapi.jsonFull HTML API reference + machine-readable OpenAPI 3.1 spec (both generated from src/apidata.ts)
/abuse, /terms, /privacy, /.well-known/security.txtAbuse reports, terms of service, privacy, vulnerability disclosure

All JSON responses share one envelope: { success, data } on success, { success: false, error: { code, message } } on failure.

Realtime — the single /v2/ws

There's exactly one socket (the old /v2/lanyard/ws and /v2/plural/ws are gone). Every frame is a { type, data } object.

On connect you get connection_established. These are then pushed to every client automatically as they happen:

  • fronters_update — who's fronting changed
  • mental_state_update — mental state changed
  • device_update — a device/battery report changed
  • force_refresh — admin asked all clients to refresh

Discord presence is opt-in (keeps traffic down). Send a subscribe frame:

{ "type": "subscribe", "all": true } // every tracked user
{ "type": "subscribe", "ids": ["123…", "456…"] } // just these users

You immediately get an init_state snapshot of the presences you asked for, then presence_update frames for those users only. Subscriptions persist across DO hibernation. Send the string ping to get pong.

Presence lives in the GATEWAY DO; when it changes, GATEWAY relays it to SYSTEM, which fans it out to the clients subscribed to that user.

Caching

See the notes in each source file; the short version:

DataWhereTTL
PresenceGATEWAY DO memorynever cached — live from the gateway
PluralKit data (system, members, fronters)SYSTEM DO memoryCACHE_TTL (default 30s), busted on any switch/member/tag/status change
Discord profilesKV (PROFILE_CACHE)PROFILE_CACHE_TTL_SECONDS (default 300s, min 60), jittered ±20%; rich (userbot) fetches back off on 429 via a shared cooldown key (30–300s)
Guild invitesKV300s
Guild membershipsKV6h
Client-mod badges (Equicord)KV1h, stale fallback
Minecraft generalKV5min; vanilla-cape registry kept permanently

HTTP Cache-Control: JSON API responses are no-store (never edge/browser cached). HTML pages (/docs, /abuse, /terms, /privacy), /openapi.json, and /v2/contribapi are public, max-age=3600. All responses carry security headers (HSTS, nosniff; CSP + frame-deny on HTML).

Setup

1. Discord application

  1. https://discord.com/developers/applicationsNew ApplicationBot.
  2. Reset Token, copy it (this is DISCORD_BOT_TOKEN).
  3. Under Privileged Gateway Intents, enable PRESENCE INTENT and SERVER MEMBERS INTENT.
  4. Invite the bot to a server that contains the people you want to track (OAuth2 URL generator → scope bot). Presence is only visible for users sharing a server with the bot — same model as Lanyard.
  5. Optionally set TRACKED_GUILD_IDS in wrangler.jsonc (comma-separated) to limit monitoring to specific servers; empty = every guild the bot can see.

2. Install & deploy

# REQUIRED
bun install
# KV namespace for profile cache — paste the printed id into wrangler.jsonc
bunx wrangler kv namespace create PROFILE_CACHE
# Secrets
bunx wrangler secret put DISCORD_BOT_TOKEN
# Optional, ToS risk — only if you want the rich badges:
bunx wrangler secret put DISCORD_USER_TOKEN
# Optional 2nd/3rd userbot:
bunx wrangler secret put DISCORD_USER_TOKEN2
bunx wrangler secret put DISCORD_USER_TOKEN3
# Local dev (uses .dev.vars — see .dev.vars.example)
bun dev
# Production
bun deploy

Other secrets for the plural-system half (JWT_SECRET, SYSTEM_TOKEN, TURNSTILE_SECRET, ADMIN_PASSWORD, DOUGH_BOT_TOKEN, BATTERY_API_KEYS) go in .dev.vars locally and wrangler secret put <NAME> in production — .dev.vars.example documents all of them.

3. Useful commands

bun typecheck # tsc --noEmit
bun tail # live production logs# Update X-Super-Properties when Discord bumps the client build:
bun decode "X-Super-Properties: [BASE64 HERE]"

Configuration (wrangler.jsonc vars)

VarPurpose
TRACKED_GUILD_IDSComma-separated guilds to monitor; empty = all the bot sees
MEMBERSHIP_GUILD_IDSGuilds to resolve per-user membership for; falls back to TRACKED_GUILD_IDS
PROFILE_CACHE_TTL_SECONDSProfile KV cache TTL (min 60)
CACHE_TTLPluralKit cache TTL in seconds
DISCORD_CLIENT_BUILD_NUMBERFor userbot X-Super-Properties (update with bun decode)
PRONOUNDB_API_BASE, TIMEZONE_API_BASE, REVIEWDB_API_BASEThird-party enrichment sources, overridable for self-hosted forks
BASE_URL, CORS_ORIGINSPlural-system base URL and CORS allowlist

Abuse, privacy & security

  • Terms:/terms — the short version: be reasonable with request volume or your IP gets blocked.
  • Privacy:/privacy — what's stored (guestbook entries, anonymous visit counts, brief Discord/Minecraft caches) and how to get it removed.
  • Report abuse / request data removal:/abuse or email abuse@doughmination.win.
  • Opt out of presence tracking: leave the Discord server(s) the bot is in, or email with your Discord user ID to be blocked from lookups.
  • Vulnerability disclosure: email the address above; machine-readable contact at /.well-known/security.txt. Responsible disclosure appreciated.

Licence

ESAL-2.1.

About

My personal API, which I dont mind people hooking into

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages