📡 Gorgonzola API

Push and read Project Gorgon community + world data from your own bot or tool.

These are the same open HTTP endpoints Gorgonzola's own bot and pages use. Most are public and need no key. If your server already has a bot that knows the daily blood target, Aurest mission, or Warden Alerts, you can push them here so players on your world see live data, and pull anything below back for your own commands.

Not writing a bot? The Gorgon Monitor is a one-file Python program that already speaks every reporting endpoint below. It reads the logs the game writes on your own PC and posts the day's world data for whichever world you play on. Its full source is printed on that page, and it is worth reading as a working example of these endpoints.

Jump to: Conventions Reporting (write) World & daily data (read) Casino arena tips Barters Community observations Chat-bot commands Live schema Restricted endpoints Rate limits

Conventions

Base URL — https://gorgonzola.spellboundtavern.com. All paths below are relative to it.

Two families:

Servers — almost everything is per-world. Pass ?server=:

strekiosmiraverrelaethdrevaarisetsu

On /api/*, an unknown or missing server returns 400 (except globally-scoped endpoints like barters) — note that ?server= present but empty is an unknown server, not a missing one, so omit the parameter rather than sending a blank. Retired world names still resolve to the world that replaced them. On /nb/* an unknown or missing value leniently falls back to the default world (default_server in the live schema) so a chat command never errors.

Auth — each endpoint is tagged: open no key needed · ip-restricted owner-only (site admin IP) · token bearer token shared with the site owner. Only open endpoints are meant for third-party use.

Reporting — push the day's community data

Validated to a known-good set, short anti-spam cooldown, last-write-wins, resets daily (Eastern). A bad report is low-impact and self-correcting — but please only report what you've actually seen in-game.

Blood target

POST/api/blood-target?server=<server>open

Body {"target": "Giant"}. Must be a valid blood target (the in-game season name, e.g. Big Cat, not the anatomy); unknown → 400.

curl -X POST "https://gorgonzola.spellboundtavern.com/api/blood-target?server=miraverre" \
     -H "Content-Type: application/json" -d '{"target": "Giant"}'

Aurest mission

POST/api/aurest?server=<server>open

Body {"mission": "Rahu Sewers"}, validated to the known mission set.

Agrashab's bounty

POST/api/agrashab?server=<server>open

Body {"target": "Winter War-Trolls"}, validated to the known bounty set. The Fae Realm god's daily group bounty; same shape as Aurest.

Warden Alerts

POST/api/warden-alerts?server=<server>open

Body {"alerts":[{"npc","zone","threat","ends","ends_at"}]} — the full active list (replaces the day's set), up to 10. Fields are optional strings; ends_at is an ISO/UTC timestamp used to auto-expire an alert.

curl -X POST "https://gorgonzola.spellboundtavern.com/api/warden-alerts?server=miraverre" \
     -H "Content-Type: application/json" \
     -d '{"alerts":[{"zone":"Gazluk","threat":"Orcs massing","ends_at":"2026-08-10T04:00:00Z"}]}'

Plain-text alternative (GET)

If your bot can only URL-fetch, the same three reports exist as validated GET commands returning a short confirmation:

ReportGET command
Blood/nb/blood/set?t=<target>&server=<server>open
Aurest/nb/aurest/set?m=<mission>&server=<server>open
Agrashab/nb/agrashab/set?m=<target>&server=<server>open
Warden/nb/warden/set?w=<alert>&server=<server>open
Druid event/nb/druid_event?time=<hh:mm>&server=<server>open
Barter/nb/barter/set?npc=&get=&give=Ruby,Garnet:2&getqty=open
Feedback/nb/feedback?text=<message>&kind=feedback|requestopen

These are the same reports as the JSON POSTs above and share their validation and cooldowns — a value outside the known set is rejected, not stored. Two behave a little differently: /nb/druid_event both reports (with ?time=) and reads (without), and earliest sighting wins rather than latest; /nb/barter/set only fills a trade the site lists as "exchange unknown" and never overrides curated data. /nb/feedback is rate-limited per who (else per IP).

Optional &who=<your-bot-or-user-id>: the first time a who reports with an explicit &server=, that server is remembered for them. Their later commands that omit &server= default to it — so a per-world bot can set server= once and drop it after. An explicit &server= always wins.

World & daily data — read, JSON

All open GET. Per-server ones need ?server=.

GET/api/moon-phasesopen

Moon phase per date, ±60 days. {"phases": {"2026-08-09": "Waning Crescent", ...}}. The site's single source of truth for the phase; the in-game moon drives portals, mushrooms, boat destinations, etc.

GET/api/qatikopen

Today's Qatik dungeon. {"today","tomorrow","quest","index","date"}. Computed, not reported — always available.

GET/api/blood-target?server=<server>open

{"target":"Giant","updated_at":"..."}, or {"target":null} if unreported.

GET/api/aurest?server=<server>open

{"mission":"Rahu Sewers","date":"...","stale":false}. stale:true means the last report is from a previous day.

GET/api/agrashab?server=<server>open

{"target":"Winter War-Trolls","date":"...","stale":false}. Agrashab's daily bounty; stale:true means the last report is from a previous day.

GET/api/warden-alerts?server=<server>open

{"alerts":[{"npc","zone","threat","ends","ends_at"}],"stamp":"..."}; expired alerts filtered out.

GET/api/weather?server=<server>open

{"zones":[{"zone","weather"}],"stamp","expiry_ms","source_name"}.

GET/api/garden?server=<server>open

{"events":[{"crop","zone","active","dur_secs"}],"stamp"} (Gardening Almanac).

GET/api/newsopen

The in-game news feed (the launcher's own rich-text markup), text/plain. This remains the default and is unchanged.

?format=json returns the same feed parsed into entries: {"entries":[{"date","title","kind","sections":[{"heading","blocks"}],"html"}]}. date is ISO-8601, kind is major or minor, and html is the rendered body using only a small tag subset (p ul li strong em a br h3 code).

Parsing it correctly is harder than it looks, which is why this is offered rather than left to each client. The feed reuses <color=…> for two different things: an entry title and a section heading inside an entry. A line only starts a new entry if it carries a <size=> wrapper or a leading date; otherwise it belongs to the entry already open. Treat every coloured line as an entry and one update becomes twenty dateless ones, which is exactly what this site did until 2026-09-18.

GET/api/themeopen

Resolved site theme. {"theme","mode","updated_at"}. Mostly for the site's own nav; included for completeness.

Casino arena tips — read, JSON

The day's Red Wing Casino fight tips and the edge they imply. Tips reset daily (Eastern), so everything here is about today on one world.

GET/api/gambling?server=<server>open

Read tips and nothing else unless you specifically want to know where a tip came from. It is the union of every live source, already merged and already filtered for staleness — the same set the site and /nb/fight compute from, so your numbers will match theirs.

{
  "date": "2026-09-04",
  "fighters": ["Corrrak", "Dura", "Gloz", "Leo", "Otis", "Ushug", "Viz"],
  "tips":      {"plus5:Corrrak": "...", "minus5:Viz": "...", "edge:Gloz:Corrrak": "..."},
  "tip_count": 15,
  "tip_budget": 15,
  "sources":   ["sheet", "redwing"],

  "submitted":          {"plus5:Corrrak": "<ip-hash>"},
  "sheet_tips":         {"minus5:Viz": "<submitter>"},
  "sheet_fetched_at":   "2026-09-04T05:02:50Z",
  "redwing_tips":       {"edge:Gloz:Corrrak": "<submitter>"},
  "redwing_fetched_at": "2026-09-04T06:03:16Z"
}

Tip keys — plus5:<fighter> and minus5:<fighter> are that fighter's own ±5% modifier; edge:<a>:<b> means a is favoured over b by 10%. The value is who reported it (an IP hash for site reports, a name for a pulled feed) — treat it as opaque and only test truthiness.

Edge between two fighters, matching the site exactly:

mod(x)   = (plus5:x ? +5 : 0) + (minus5:x ? -5 : 0)
edge(a,b) = mod(a) - mod(b) + (edge:a:b ? +10 : 0) - (edge:b:a ? -10 : 0)

Positive favours a, negative favours b, zero is even. Or skip the math and call /nb/fight below, which returns the answer as a sentence.

odds is the board, precomputed: one entry per unordered matchup, {"a","b","edge","a_odds","b_odds"}. edge is the same ±% /nb/fight quotes (positive favours a); a_odds/b_odds express it as percentages that sum to 100, which is usually what you want to show. It is deliberately unsorted and unfiltered — sorting and filtering are the page's job — and it is derived, so you can ignore it entirely and compute from tips with the formula above. A day with no tips reported yet returns an empty list, meaning every matchup is even.

suppressed lists tip keys someone has deleted for the rest of the day. They are already excluded from tips; the field is there so you can tell "never reported" from "reported and withdrawn". A deleted tip stays gone even if the sheet or redwing re-reports it on the next sync.

Two more casino reads are open below — the live matchup (/api/current-fight) and the settled-bet record (/api/bets).

The broken-out sources are for attribution and staleness display, not for summing — adding them yourself double-counts tips that two feeds both report, and re-implements the day-boundary check. submitted is what players reported on the site; each <kind>_tips is a pulled feed, shown raw next to its <kind>_fetched_at so you can tell "no tips today" from "the pull broke". A feed whose stamp is not today is excluded from tips but still listed here. sources names the feeds that exist, so a new one appears in tips with no change on your end.

GET/nb/fight?a=<fighter>&b=<fighter>open

The same answer as one chat line, winner first so the pick survives a client that truncates: 🥊 Gloz +10% · Corrrak vs Gloz · (15/15 tips reported) → <link>, or 🥊 Even, no reported edge · …. Free-text ?q=corrrak gloz also works, and ?list=fighters returns today's roster. With no a/b it answers the bookie's live matchup if one is posted (🥊 Gloz +10% · Now: Corrrak vs Gloz (starts in 3m) → <link>), otherwise usage text — so don't treat a no-argument call as a help request.

GET/api/current-fight?server=<server>open

The bookie's live matchup, community-reported by the chat monitor as Kyuroma calls it. {"fight":null} when nothing is on — which is most of the time, so handle the null rather than assuming a fight object.

{"fight": {"a": "Otis", "b": "Dura", "starts_in": 174, "ends_in": 234, "starts_at": ..., "ends_at": ...}}

starts_in is seconds until it begins and ends_in seconds until the record disappears. Both are computed per request and floored at 0, so a client whose clock is off still counts down correctly. The shape of a fight's life: betting is open for up to 8 minutes while starts_in counts down; at 0 the fight is under way and betting is shut; one minute later the record is gone and the next matchup can be reported. There is no "fight over" event to wait for, so expire it on ends_in rather than holding the last matchup you saw. Pair it with /api/gambling above to show the edge for the matchup that is actually live, which is what /nb/fight with no arguments does.

GET/api/betsopen

Community record of settled arena bets — win/loss only, no wager amounts are collected or stored. Optional ?server= and ?char= filters. Global by default (not per-server).

{
  "count": 8,
  "fighters":    {"Gloz": {"wins": 2, "losses": 1, "fights": 3, "win_rate": 0.6667}, ...},
  "totals":      {"bets": 8, "wins": 4, "losses": 4, "win_rate": 0.5},
  "predictions": {"calls": 6, "correct": 4, "accuracy": 0.6667,
                  "by_edge": {"5": {"calls": 4, "favored_wins": 2, "win_rate": 0.5}, ...}},
  "recent":      [ ...up to 20 newest records... ]
}

predictions is the interesting part: each bet is stored with the site's community edge at the time it was placed, so by_edge answers whether a claimed +10% edge really wins more often than a +5% one. Keyed by edge magnitude as a string; win_rate there is how often the favoured fighter actually won. Sample sizes are small — check calls before drawing conclusions.

Fighter names are not validated against a fixed roster (a new fighter should never be dropped), so treat the fighters keys as open ended — note redwing.bet's Vizlark and our Viz can both appear.

Barters — global (not per-server)

GET/api/bartersopen

The community barter layer: {npc_key: {get_iname: {give, get, note, reported_at}}}. Merged under the site's curated barter data — a community report only fills an "exchange unknown" trade, never overrides a curated one.

POST/api/bartersopen

Report one exchange: {"npc","get":{"item","qty"},"give":[{"name","qty"}]}. Validated — real NPC, the get item must be one that NPC actually barters, each give name must resolve to a real item, qtys 1–9999. 20s per-IP cooldown. (The singular /api/barter is an accepted alias.)

Community observations — crowd-sourced game data

Aggregate layers built from what players actually see in-game. Reports (POST) fold into a shared store and surface on the site; the matching reads are open JSON. Global, not per-server.

Mob loot drops

POST/api/lootopen

Report looted corpses and their drops, batched: {"events":[{"type":"corpse"|"loot","mob","item?"}]}. A corpse event increments that mob's looted-corpse count (the drop-rate denominator); a loot event records one item (its InternalName) dropped by that mob (display name). Aggregated into the observed-drop DB behind bestiary.html. Up to 2000 events per call; 2s per-IP cooldown. It is a dumb incrementer — report each corpse exactly once (don't re-send on a rescan) or the drop rates inflate. (A submission token may gate this later; open today.)

Two optional extras feed the blood-type inference below, and nothing else. ?server= names the world the kills happened on (a world the site doesn't track is accepted and simply infers nothing), and a per-event "at", a unix timestamp of when the drop was seen, keeps a batch that waited out an outage from being filed under the wrong day's blood season. Both are optional; a client that sends neither is unaffected.

curl -X POST "https://gorgonzola.spellboundtavern.com/api/loot" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"type":"corpse","mob":"Sickly Wolf"},
                 {"type":"loot","mob":"Sickly Wolf","item":"WolfPelt"}]}'
GET/api/bestiary-observedopen

The aggregate, resolved for display: {mob: {corpses, items:[{iname, name, icon, count, rate, first, last}], updated}}. rate = count / corpses; items are name/icon-resolved from the game data and sorted by count.

GET/api/bestiary-blood-observedopen

Vampire blood types the community found by feeding: {mob: [{blood_type, target, count, days, reporters, confirmed, unknown_mob, first, last}]}. A creature's blood type is not in the game data anywhere, so the site's bestiary only ever guessed it from anatomy and name. Vampire loot answers it directly: it comes from feeding, and feeding pays out on the day's blood target, so when POST /api/loot reports a Vampire-keyworded item (Lifeblood, Hematic Ichor, a Hate Pearl) off a mob the bestiary doesn't already file under today's season, that mob is recorded here.

blood_type is the bestiary's own vocabulary and target the blood-target name the site stores; they differ for two (Elven/Elf, Non-Ruminant Ungulate/Ungulate), and both are sent so no client needs to carry that mapping. confirmed means the mark cleared the floor of two distinct days and two distinct reporters; ?confirmed=1 returns only those. unknown_mob marks a creature the bestiary has no record of at all.

Death XP

GET/api/death-xpopen

Observed death-XP overlay: {deaths: {slug: {first_xp, repeat_xp, xp_values}}}. The canonical first-death XP, the highest repeat award, and xp_values, every award value anyone has reported for that cause. Repeat XP decays toward 0 over successive deaths, so that set is the decay ladder (e.g. [0, 1, 5, 50]) and it is the part worth pooling. The first two fill the unknown xp/rxp values on dying.html.

POST/api/death-xpopen

Report what a death paid you: {observations: [{key, first_xp?, repeat_xp?, xp_values?}]}. key is the cause-of-death slug (the cause_of_death_* id, lowercased); xp_values is every award value you have seen for it.

Open, because this is the one dataset that cannot come from the game's files: somebody has to die of the thing and read the number. Nobody dies of everything. Per cause the merge keeps the max of each headline number and the union of the value set, so re-sending your whole history, or two people reporting the same cause, can never lower or lose a value. Values outside 0 – 100000 and malformed slugs are dropped. Max 500 observations a push, 2s per IP.

The Gorgon Monitor does this for you, reading ProcessDeathMessage out of Player.log.

Chat-bot commands — /nb/*, plain text

Every /nb/* endpoint returns a short text/plain line built for a Twitch/Discord chat message. They're open. This list can grow — GET /nb is the live index (?fmt=json for structured), and GET /nb/commands emits copy-paste Nightbot !addcom lines. Highlights:

CommandReturns
/nb/moonMoon phase + time to the next phase. ?phase=full → time until that phase.
/nb/clockIn-game time + next time-of-day.
/nb/qatikToday's Qatik dungeon (+ tomorrow).
/nb/dailiesCombined "today in PG": moon + Qatik + Aurest + Agrashab + blood + warden + druid. Takes ?server=.
/nb/aurest · /nb/agrashab · /nb/bloodToday's Aurest mission / Agrashab bounty / blood target. ?server=.
/nb/warden_alertActive Warden Alerts (threat + zone + time left).
/nb/portal · /nb/rahuboat · /nb/darkchapelMoon-tied Casino portal, Rahu boat destination, Dark Chapel location.
/nb/mushroomMushrooms growing robust in the current moon phase.
/nb/npc?q= · /nb/quest?q=NPC / quest lookup + a deep link.
/nb/teleport?zone=&loc=Teleport-machine code(s). ?list=zones / ?list=locs&zone= for dropdowns.
/nb/equipment?q= · /nb/augment?q=Item / augment summary + a deep link.
/nb/fight?a=&b= · /nb/gamblingCasino fight odds / gambling page link. ?server=.
/nb/druid_event?time=Community druid-event tracker (Discord-only; reply uses Discord <t:…> timestamps). A ?time= (xx:12/10:12) reports a sighting; no time reads the current one. Per ?server=, self-clears after 1h, earliest sighting wins.
/nb/next_event?category=&server=What's next on the world events board.
/nb/craft?q=&variant= · /nb/recipe_source?q=What a recipe takes (skill, level, top-level materials) / where the recipe itself is learned. variant ∈ normal/enchanted/max-enchanted.
/nb/skill_source?q=Where an ability comes from — trainer, book, altar, quest, research, or a skill level-up.
/nb/barter?q=Barter picker helper (feeds the report form below).
/nb/site?p= · /nb/gorgon?p= · /nb/support · /nb/gamblingLink dispatchers — a Gorgonzola page, a new-player link (buy / Discord / wiki), the support page, the gambling page.
/nb/register_me?server=&who= · /nb/register_discord_server?server=&guild=Set the caller's (or, admins, the whole Discord guild's) default game world, so later commands need no ?server=. These write — see the note under the table.

Dropdown / autocomplete helpers

Several lookups take ?list= instead of a query and return Options: a, b, c — built for a Discord slash-command choice list: /nb/fight?list=fighters, /nb/npc?list=npcs&q=<prefix> (people only, so signs and altars stay out of the picker), /nb/quest?list=quests&q=, /nb/teleport?list=zones and ?list=locs&zone=. Quest and NPC names contain commas ("Revenge For Slavery, Pt. 1"), which the comma-delimited form splits into bogus choices — pass &fmt=lines on those two for the newline-delimited version.

Stream-tracker reads

These report the site owner's currently-active character, fed by the chat monitor, and are what a stream overlay or !lastdeath-style command reads. They are open but describe one specific player, not your own: /nb/last_death, /nb/last_skill, /nb/last_quest, /nb/deaths (?all=1 for the all-time tally), /nb/age, and /nb/char/<char>?stat= for a named character.

Writes

Everything above is a read. The commands that write are /nb/{blood,aurest,agrashab,warden}/set, /nb/barter/set, /nb/feedback and /nb/druid_event?time=, all listed with their parameters in Reporting above, plus the two /nb/register_* commands in the table above (which write only the caller's own world preference). They are open but validated and rate-limited; a value outside the known set is rejected rather than stored. Pass &who= (and &guild= from a Discord bot) so the per-user and per-guild world memory works.

Some commands are Discord-only by convention (e.g. /nb/craft, /nb/skill_source, /nb/druid_event, /nb/register_me) — they work as URLs but are kept off the Nightbot !addcom list. GET /nb is always the authoritative, current set.

Live schema — don't hardcode valid values

Blood targets and Aurest missions change as the game does. Fetch the valid sets instead of hardcoding them — this is the source of truth:

GET/api/reporting-schemaopen

{"site","servers","default_server","aliases","blood_targets","aurest_missions", "agrashab_targets","garden_crops","fighters","cooldown_seconds"}. aliases maps a retired world onto the one that replaced it, so an old ?server= keeps resolving; cooldown_seconds is per report kind (blood / aurest / agrashab / warden / druid) — read it rather than guessing a retry delay.

fighters is the casino arena roster, for a client that cannot read our database — which is any client but ours:

"fighters": {
  "roster":        ["Corrrak", "Dura", "Gloz", "Leo", "Otis", "Ushug", "Viz"],
  "aliases":       {"vizlark": "Viz", "corrak": "Corrrak", "goz": "Gloz", ...},
  "redwing_ids":   {"Corrrak": 1, ..., "Viz": 7},
  "redwing_names": {"Viz": "Vizlark"}
}

roster is the board. aliases is wider than the roster on purpose — it maps a lowercased spelling (typos included) to a canonical name and covers fighters who are not on the board, so a tip naming one still resolves instead of being dropped. redwing_ids/redwing_names are redwing.bet's parallel vocabulary for the same fighters; they call our Viz Vizlark, which is worth knowing if you read both sources (the fighters keys of /api/bets can carry either spelling for historical rows).

GET/api/garden-optionsopen

The garden crop → emoji map: {"crops":{"apple":"🍎", ...},"default_emoji":"🌱"}. Same idea as above — adding a plant is one data edit here, so don't hardcode the crop list. default_emoji is what an as-yet-uncurated crop renders as, so an unknown crop is a display gap, never an error. Also embedded in reporting-schema as garden_crops; this is the standalone read.

Valid blood targets

loading…

Valid Aurest missions

loading…

Restricted endpoints — not for third parties

Listed so you don't waste time on them. These exist but are gated:

Rate limits & good citizenship

Questions, or want a data type added? Reach out via the feedback form or the Discord bot.