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.
Base URL — https://gorgonzola.spellboundtavern.com. All paths below are relative to it.
Two families:
/api/* — JSON in, JSON out. Reads are GET; reports are POST with a JSON body./nb/* — GET-only, plain text sized for a chat message, for Nightbot / StreamElements / Discord bots that can only do a URL fetch. GET /nb is a live self-listing index of the whole family.Servers — almost everything is per-world. Pass ?server=:
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.
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.
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"}'
Body {"mission": "Rahu Sewers"}, validated to the known mission set.
Body {"target": "Winter War-Trolls"}, validated to the known bounty set. The Fae Realm god's daily group bounty; same shape as Aurest.
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"}]}'
If your bot can only URL-fetch, the same three reports exist as validated GET
commands returning a short confirmation:
| Report | GET 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|request | open |
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.
All open GET. Per-server ones need ?server=.
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.
Today's Qatik dungeon. {"today","tomorrow","quest","index","date"}. Computed, not reported — always available.
{"target":"Giant","updated_at":"..."}, or {"target":null} if unreported.
{"mission":"Rahu Sewers","date":"...","stale":false}. stale:true means the last report is from a previous day.
{"target":"Winter War-Trolls","date":"...","stale":false}. Agrashab's daily bounty; stale:true means the last report is from a previous day.
{"alerts":[{"npc","zone","threat","ends","ends_at"}],"stamp":"..."}; expired alerts filtered out.
{"zones":[{"zone","weather"}],"stamp","expiry_ms","source_name"}.
{"events":[{"crop","zone","active","dur_secs"}],"stamp"} (Gardening Almanac).
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.
Resolved site theme. {"theme","mode","updated_at"}. Mostly for the site's own nav; included for completeness.
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.
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.
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.
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.
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.
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.
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.)
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.
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"}]}'
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.
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.
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.
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.
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:
| Command | Returns |
|---|---|
/nb/moon | Moon phase + time to the next phase. ?phase=full → time until that phase. |
/nb/clock | In-game time + next time-of-day. |
/nb/qatik | Today's Qatik dungeon (+ tomorrow). |
/nb/dailies | Combined "today in PG": moon + Qatik + Aurest + Agrashab + blood + warden + druid. Takes ?server=. |
/nb/aurest · /nb/agrashab · /nb/blood | Today's Aurest mission / Agrashab bounty / blood target. ?server=. |
/nb/warden_alert | Active Warden Alerts (threat + zone + time left). |
/nb/portal · /nb/rahuboat · /nb/darkchapel | Moon-tied Casino portal, Rahu boat destination, Dark Chapel location. |
/nb/mushroom | Mushrooms 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/gambling | Casino 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/gambling | Link 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. |
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.
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.
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.
Blood targets and Aurest missions change as the game does. Fetch the valid sets instead of hardcoding them — this is the source of truth:
{"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).
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.
Listed so you don't waste time on them. These exist but are gated:
POST /api/stream/event) — a bearer token shared privately with the site owner; drives the OBS overlay + /nb/last_death etc.GET /api/reporters, POST /api/reporters/block): the owner's view of who is writing to the open endpoints, and the blocklist behind it. It records a one-way hash of the reporting address, never the address itself, exactly as the analytics page describes./auth/*), gift planner data, egg/vamp map markers, and the write half of the casino tips (POST /api/gambling, /api/gambling/tip, and the feed refreshes) — tips are entered on the site so they carry reporter attribution and the daily budget. The read (GET /api/gambling above) is open.cooldown_seconds in the schema; a repeat inside the window returns 429 / "Just reported"). Report on a real change, not on a timer.User-Agent so a misbehaving client can be reached rather than just blocked. Writes are counted per reporter (by hashed address, never a stored address) and a client that floods or sends junk can be blocked. Being reachable is what makes a conversation possible first.Questions, or want a data type added? Reach out via the feedback form or the Discord bot.