public/events.json is the whole product in one file. It's served CORS-open with
no key and no rate limit — build on it freely.
curl https://hackathons.dev/events.jsonRegenerated by GitHub Actions four times a day (00:00, 06:00, 12:00, 18:00 UTC).
Check generatedAt rather than assuming freshness.
| Field | Type | Notes |
|---|---|---|
generatedAt |
ISO 8601 string | When the ingest ran |
count |
number | Always equals events.length |
seeded |
boolean? | Present and true only for fixture-seeded local builds |
sources |
Source[] |
Per-source health for this run |
events |
Event[] |
Sorted: open first by closest deadline, then upcoming |
| Field | Type | Notes |
|---|---|---|
id |
string | devpost, hackclub, mlh |
name |
string | Display name |
url |
string | The platform's own listing page |
count |
number | Events contributed this run |
ok |
boolean | false means the source failed and its events are missing |
Always check ok. A source can fail without the file failing — that's by
design, but it means coverage is silently incomplete for that run.
| Field | Type | Notes |
|---|---|---|
id |
string | Globally unique, {source}-{sourceId}. Stable across runs. |
source |
string | Source id |
sourceName |
string | Source display name |
alsoOn |
string[] | Other sources carrying the same event (see Deduplication) |
| Field | Type | Notes |
|---|---|---|
title |
string | Never empty |
url |
string | Always https://. Links to the organizer, not to us. |
imageUrl |
string | null | null when absent or a known placeholder |
organizer |
string | null | |
status |
"open" | "upcoming" | "ended" |
ended events are filtered out before publishing |
| Field | Type | Notes |
|---|---|---|
startsAt |
ISO 8601 | null | |
endsAt |
ISO 8601 | null | Submission deadline. The primary sort key. |
deadlineLabel |
string | null | Source's own phrasing, e.g. "24 days left" |
dateLabel |
string | null | Human range, e.g. "May 19 - Aug 17, 2026" |
Dates are best-effort. Devpost publishes only display strings, so
scripts/lib/parse.mjs reconstructs ISO timestamps from them. Roughly 100% parse
cleanly today, but treat null as genuinely unknown rather than assuming a
default. Times default to 23:59 UTC when the source gives no time component.
| Field | Type | Notes |
|---|---|---|
format |
"online" | "in-person" | "hybrid" |
|
locationLabel |
string | null | "Online", or a venue/city string |
city, country, countryCode |
string | null | Populated mainly by Hack Club |
lat, lng |
number | null | Hack Club only — pre-geocoded, ready for map views |
| Field | Type | Notes |
|---|---|---|
prizeUsd |
number | null | Approximate USD. For sorting and filtering only. |
prizeLabel |
string | null | Original currency and amount, e.g. "₹3,000". Display this. |
prizeCurrency |
string | null | ISO 4217, e.g. USD, INR, CAD |
cashPrizeCount |
number | Number of distinct cash prizes |
prizeUsd uses fixed conversion rates that are refreshed rarely. It exists to
make prizes comparable across currencies — never present it as an exact
conversion. prizeLabel is null when the prize pool is zero.
| Field | Type | Notes |
|---|---|---|
themes |
string[] | Source-provided tags, e.g. "Machine Learning/AI". Empty for MLH and Hack Club. |
audience |
"high-school" | "university" | "open" |
Inferred; see below |
inviteOnly |
boolean | Filter these out for general audiences |
beginnerFriendly |
boolean | From an explicit tag, or true for high-school events |
participants |
number | null | Registration count. Devpost only. |
audience is inferred, not authoritative. Hack Club events are always
high-school and MLH events default to university, both of which are reliable.
Devpost events are classified by keyword matching on title and organizer, which
is a heuristic — verify on the listing page before relying on it.
The same hackathon regularly appears on several platforms — MLH-sanctioned events often have a Devpost page too. Events are matched on a normalized title (stripped of years and filler words like "hackathon") plus start day.
When a match is found the richer record wins — SOURCE_RANK in
scripts/ingest.mjs prefers Devpost, since it carries prize and participant data
the others don't. The losing source is preserved in alsoOn rather than dropped,
so nothing is silently hidden.
This means alsoOn: ["Major League Hacking"] tells you the event is MLH-sanctioned
even though the record came from Devpost.
scripts/validate.mjs runs before anything is published and blocks the commit
on:
- any required field missing
- duplicate
id - non-HTTPS
url - invalid
format - a deadline more than 24 hours in the past
- a drop of more than 50% in event count versus the last published run
That last one is the important one. A sudden cliff almost always means a source silently changed its API shape, and stale-but-correct data beats fresh-but-broken data every time.
It warns (without blocking) when deadline coverage falls below 80%, when more than 60% of events lack images, or when any source failed.
The schema is additive — new fields may appear, existing ones won't change type or
disappear without a note here. id values are stable across runs, so they're safe
to store.
If you build something on this, an issue or a link back is welcome.