`; cell 1 is the row label (`BATTLESUIT SUPPLY A`, `EVENT 3`)
and every bar carries `col-span-N`, with bare `
` spacers standing in for
an offset start. So a bar's start week is `1 + Σ` the spans before it and its length is its own span,
and the week columns are in the header row. A parser would be positional rather than textual and is
perfectly writable. The blocker is what the numbers mean, not how to read them.
**What would change this:** the site stating a year, and stating a real regional schedule rather than
an estimate over the CN one — plus a stable URL for the current version, or an index page naming it.
Any two of the three still leaves the other. No alternative HI3 source was looked for in this pass, so
the game is unsourced rather than declined outright.
**Superseded 2026-08-27.** The alternative this section did not look for exists — `arustats.com`,
§ 14 below — and it answers three of the four objections above. `marisaimpact.com` itself stays
declined on all four; nothing here has been re-tested and this verdict is not the live one for the
game.
## 14. Honkai Impact 3rd — BUILT 2026-08-27, on estimated week buckets, by decision
**Source:** `https://www.arustats.com/en-us/hi3/timeline` — AruStats' per-version timeline. Assessed
on request as an alternative to the `marisaimpact.com` page § 13 declined, and built with one of
§ 13's four objections still standing.
**Conduct is clean, and better than most things here.** `robots.txt` is `User-agent: *` / `Allow: /`
with `/hi3/*` and `/en-us/*` named explicitly, no `Disallow` anywhere, no `Content-Signal` header and
none of the named AI-crawler blocks; `Crawl-delay: 1`, far below our 6h floor. Verified through the
real `RobotsCache` and the runner's own User-Agent rather than `curl` — `robots.txt ok`,
`crawlDelayMs: 1000` — per AGENTS.md § Fandom on why the instrument matters. The page carries its own
"not affiliated with miHoYo" attribution line, and the host advertises a sitemap.
**Three of § 13's four objections are answered:**
| § 13 objection | Here |
|---|---|
| Dates are CN estimates redrawn for regions | The grid is headed `GLB/SEA` — our readers' server, not a CN schedule with an estimate over it |
| No year appears anywhere | `scheduleDates` states full dates with years — `2026-8-20`, `2026-10-22` |
| The URL is per-version with no stable route | **`/en-us/hi3/timeline` answers `307` to the live version.** The site names its own current version server-side; the runner follows it and no URL ever needs editing |
| The unit is a week column, not an event boundary | **Still true.** Unchanged, and it is the whole cost below |
**The objection that stands.** No event on the page states a date. Events are bars carrying
`startWeek` / `endWeek` *integers* into a nine-column week grid, and the header over that grid reads
`ESTIMATED WEEK`. So every boundary this source publishes is the edge of a bucket the site estimated.
That is weaker than the day-precision reading Game8 and Infinity Nikki get — there the page printed a
date per event and we declined only to invent a time of day for it — and § 13 declined
`marisaimpact.com` on exactly this ground.
**It was built anyway, and that is a decision rather than an oversight** (repository owner,
2026-08-27, with the cost stated before the adapter was written). The reasoning: Honkai Impact 3rd
had no source at all, this page is otherwise healthy and current, and a lane of approximate
six-week windows was judged better than no lane. Be exact about what that buys and what it costs:
- **What is honest about it.** The week grid's *outer* boundaries are real and independently
checkable: v8.9 reads `2026-6-25` → `2026-8-20` and v9.0 picks up at `2026-8-20` → `2026-10-22`,
which are the version's actual maintenance dates and chain exactly. Sixteen bars on the page yield
sixteen events, so nothing is silently dropped.
- **What is not.** Where a banner swaps *inside* a version is the estimated part, and those are most
of the boundaries. `startsAt` is also half of every event ID, so a week grid the site revises moves
IDs and orphans completion marks — a hazard no wiki source here carries.
- **How the fact is carried.** `ESTIMATE_CONFIDENCE` (0.4) on every event, against the 0.85–0.95 a
date-stating source earns. `mergeEvents` prefers the higher number, so a real HI3 source outranks
this one the moment one is added, with no cleanup step to forget.
- **The gap left open.** `confidence` is read nowhere in `src/client/`, so nothing on screen tells a
reader this lane is estimated while every other lane is announced. Closing that needs a schema
field or a client change — a design question, not adapter work — and it is the obvious next step
if this source stays.
**What would retire it:** any HI3 source that states a date per event. That is a strictly lower bar
than § 13's list, and this source outranks nothing — it is the floor, not the ceiling.
**The markup, so nobody re-derives it.** The site is Next.js and server-renders the whole schedule
into `__NEXT_DATA__` (`props.pageProps.timeline`), so `parsers/arustats.ts` reads JSON and ignores the
rendered grid entirely — the bars carry their geometry in Tailwind `col-span-N` classes and their
offsets in bare spacer `
`s, which a DOM reader would have to count positionally. `endWeek` is
exclusive and runs one past the grid to mean "to the end of the version". `scheduleBosses` is the
only exactly-dated material on the page (Abyss and Memorial Arena openings, three a week) and is
deliberately unread: a recurring rotation with no end is not a deadline, and `endsAt: null` on each
would render them live-with-unknown-end forever.
**As of 2026-08-27 the blocker above is addressable.** `src/shared/recurrence.ts`
gives an event a repeat rule, and a rule supplies the boundary the rotation was
missing: an occurrence with no stated end runs until the next one opens, so
`endsAt: null` no longer means live-with-unknown-end forever. Reading
`scheduleBosses` is now a parser change rather than a design question, and it is
Phase B's first target in
`docs/superpowers/specs/2026-08-27-recurring-events-design.md`. Nothing has been
changed on the ingest side yet — `GachaEvent` does not carry a rule, and this
note is what stops the next person re-deriving the reason it does not.
## 15. Genshin Impact (Fandom) — BUILT 2026-09-12, adding a second source for a blind lane
**Source:** `https://genshin-impact.fandom.com/api.php?action=parse&page=Event&prop=text&formatversion=2&format=json` (page at `https://genshin-impact.fandom.com/wiki/Event`).
**Conduct:** Standard Fandom `robots.txt` (`Allow: /api.php?action=`, no `Disallow: /` for `*`, no `Content-Signal`). Read with the runner's client; CI can fetch it.
**Why built:** Genshin's Game8 page has never been fetchable from CI (CloudFront returns a `202` on all runners), so until now Genshin was fixture-backed in CI and could only age. Fandom provides a second, active source that scheduled refreshes can update.
**The template:** Fifth Fandom template. `Current Events` and `Upcoming Events` wikitables under `h3` headings with columns `Event | Duration | Type(s)`.
- **Title extraction:** Display text of the caption link beneath the banner image is taken. Link `title` (parent subpage) and `img alt` (uploaded image file name) are rejected to prevent corrupting event titles and IDs.
- **Run-date suffixes:** Recurring event run dates (`2026-09-14`) in subpage names are stripped so dates aren't duplicated.
- **Currency:** `latestBoundaryMs` is checked for day-precision boundaries so events aren't retired before all server regions roll.
- **Priority & IDs:** Game8 is assigned `priority: 10` while Fandom is default/0. Two live events differ slightly in naming (`To Temper Thyself and Journey Far` vs `… Cycle 5`, and `Stygian Onslaught` vs `…: Battle of the Starburst`). Game8's higher priority breaks near-match ties in `merge.ts`, ensuring existing localStorage event IDs survive and reader completion marks remain intact. Fandom contributes 10 net-new events Game8 missed, 0 date conflicts occur, and overlapping events gain corroboration bonuses. (Recorded as 11 until 2026-10-14: one was Game8's `Battle Pass - Frostfarer` beside Fandom's `Frostfarer`, the same event under two IDs. Game8's parser now drops that label. The anniversary selectors, which share no word across the two sources, pair because Game8's blurbs name the Fandom events; AGENTS.md § Fandom has the rest.)
- **336-day event:** Genshin's anniversary 5-star selection runs 336 days (`2025-10-22` → `2026-09-23`), prompting the test duration ceiling to be raised from 180 to 365 days across the test suite and ingestion specs.
## 16. hololive Dreams — the wiki lapsed (2026-10-14); nothing else can be fetched
> **Outcome: kept, and recorded as lapsed.** `holodori.wiki` answers every cycle and the parser
> still reads it, but its editors stopped adding events after 19 Sep 2026. No replacement passes
> § Method. The source stays registered with `lapsed` set (`src/ingest/adapters/index.ts`), so the
> refresh reports it as a warning rather than failing every run, and it recovers by itself if the
> editors return. Readers still see the lane's age in the staleness footer, which is the honest
> signal here.
**How it showed up.** The footer said *"hololive Dreams (21d 7h ago)"*, and refresh run 317
(2026-10-13) logged `rejected — kept previous snapshot; new body yielded 0 events (had 1)` then
`broken` after 21 failing cycles with `last status 200`. So the source was answering, not blocked.
**What the page shows.** One request on 2026-10-14 (our User-Agent, about 7h after the last CI
fetch; kept as `fixtures/holodori/holodoriwiki-events-2026-10-14.html`):
- **Same template.** `canParse` passes and the two-heading layout is unchanged. The parser is
reading the page correctly.
- **Nothing current.** `Current Events` holds only `Beginner Mission`, which has no start date and
is skipped by design (AGENTS.md § hololive Dreams). The newest row anywhere is
`A Dreamy Summer Escape`, 09/19–09/25 JST, under `Past Events`.
- **The game did not pause; the page did.** Rows had been added every week or two from 7 Aug to
19 Sep. The game's X account then ran a score challenge from 7 to 13 Oct and announced a
"Neon Sunshine Festival". Neither is on the page.
**Candidates assessed:**
| Candidate | Finding | Verdict |
|---|---|---|
| NamuWiki `hololive Dreams/이벤트` | Lists the 7–13 Oct event with times. `robots.txt` allows `/w/` for `*`, but the page answers our client `403` with `cf-mitigated: challenge` (Cloudflare's JavaScript check) | **Decline.** That is an access control; § Method and AGENTS.md § Scraping conduct rule out working around it |
| `www.hololive-dreams.com/news` (official) | No `robots.txt` (404), so nothing is disallowed. But the news list has four posts in all: launch, system requirements, and two on 29 Sep (Ver.1.2.0, the Soda Pop Lagoon area). The site posts no events | **Decline** — no event data |
| Game8 | No hololive Dreams events page found, only a launch news article | **Decline** — nothing to read |
| X (`@holo_dreams_en`), press releases | X has no fetchable surface for us. Press releases (CyberAgent, syndicated) are prose about single events, not a schedule | **Decline** |
**Worth doing instead.** `holodori.wiki` is a CC BY-SA wiki, so adding the missing events there
fixes it for every reader of that wiki, and refresh picks them up with no code change. When the
page lists events again, the refresh warns that the source is listing events again and that
`lapsed` should come out of its registry entry. Do that, and update this section.
**Re-check:** `bun run refresh --only holodori-holodoriwiki-events` once the six-hour floor has
passed. A `fetched` with more than zero events means the wiki is back.
## 17. Star Rail — Nameless Honor pass window (Fandom) — BUILT 2026-10-16
**Source:** `https://honkai-star-rail.fandom.com/api.php?action=parse&page=Nameless_Honor&prop=text&formatversion=2&format=json` (page `https://honkai-star-rail.fandom.com/wiki/Nameless_Honor`), source id `hsr-fandom-nameless-honor`. First of the pass-window sources in `docs/superpowers/specs/2026-10-16-pass-windows-design.md`.
**What it is:** Star Rail's battle pass. Event pages state no pass dates, but this page keeps a history table, one row per run, with the version each belonged to. Each row that has not ended by `ctx.now` becomes one event titled exactly `Nameless Honor`, type `other`.
**Conduct:** `robots.txt` read with the runner's own `RobotsCache` and real User-Agent on 2026-10-16: `200`, allowed. `Allow: /api.php?action=` for `*`, no `Disallow: /` for `*`; the `Disallow: /` groups name other crawlers (GPTBot, CCBot, OAI-SearchBot, ClaudeBot, ImagesiftBot, Grok*, Semrush, serpstatbot), and the only `Crawl-delay` belongs to `ias_crawler`. Recorded in the `SOURCES` entry.
**Shape:** the second `article-table` on the page, `Image | Name | Start | End | Version`, 31 rows (2023-04-26 to 2026-09-28; verified by counting the page's `
`s independently of the parser). Cells print a bare date (`September 28, 2026`). Each cell also has a `data-sort-value` with a clock (`11:00:00` starts, `03:59:59` ends) that the page ties to no zone, so it is not read.
**Precision:** day, start and end. Nothing is inferred: no clock, no region, no end for a row without one (skipped). At capture one row is live, 2026-09-28 to 2026-11-09 (4.6); each row ends on the day the next one starts. The Name cell reads `Nameless Honor `, so the title is constant and the start is the only thing that tells runs apart in the ID.
**Reuse:** the parser is `fandomtable` (`src/ingest/parsers/fandomTable.ts`). Another pass page is one `PASS_SOURCES` entry (a title, constant or built from the row's Name cell) plus a `SOURCES` entry, as ZZZ's `New_Eridu_City_Fund` is (§ 18); Genshin's `Miliastra_Pass` was tried and reverted, being in the feed already; columns are found by heading. A table headed with `Duration` is deliberately not this template (it is a version table).
## 18. ZZZ — New Eridu City Fund window (Fandom) — BUILT 2026-10-16
**Source:** `https://zenless-zone-zero.fandom.com/api.php?action=parse&page=New_Eridu_City_Fund&prop=text&formatversion=2&format=json` (page `https://zenless-zone-zero.fandom.com/wiki/New_Eridu_City_Fund`), source id `zzz-fandom-city-fund`. Second pass-window source; no parser change beyond one `PASS_SOURCES` entry (constant title).
**What it is:** the first table on the page, `Image | Name | Start | End | Version`, 20 rows (1.0, 2024-07-04, to 3.2, 2026-09-09 to 2026-10-19; counted independently of the parser). Each row not ended by `ctx.now` becomes one `other` event titled `New Eridu City Fund`, day precision. At capture (now 2026-10-16) one row is not ended (3.2) and one event is emitted. The second table (`Language | Official Name`) is not a pass table and is ignored.
**Conduct:** the `RobotsCache` gets `403` (the interstitial challenge) for this host's `robots.txt`. The repository owner read it in a browser on 2026-10-16: the `User-agent: *` group allows `/api.php?`, `/api.php?action=` and `/api.php?*&action=` and disallows only `/wiki/Special:`, `/wiki/User_talk:`, `/wiki/Template:`, `/wiki/Template_talk:`, `/wiki/Help:`, `/wiki/User:`, `/wiki/UserProfile:` (and language variants); `Disallow: /` applies only to named crawlers (SemrushBot, serpstatbot, GPTBot, ImagesiftBot, CCBot, OAI-SearchBot, ClaudeBot, GrokBot, xAI-SearchBot, xAI-Grok, GrokApp, xAI-Bot); `ias_crawler` has `Crawl-delay: 1`. With the standing `--assume-robots-on-403` decision that covers this source. The API answered `200` to the real User-Agent for the single fixture fetch.
## 19. Infinity Nikki — Mira Journey window, from its version (Fandom) — BUILT 2026-10-16
**Source:** `https://infinity-nikki.fandom.com/api.php?action=parse&page=Version&prop=text&formatversion=2&format=json` (page `https://infinity-nikki.fandom.com/wiki/Version`), source id `nikki-fandom-mira-journey`, parser `fandomversion` (`src/ingest/parsers/versionTable.ts`).
**What it is:** Mira Journey has no dated page. The `Version` page dates *versions*: three tables headed `Version Name | Version Number | Release Date | End Date | Duration` (2.x, 1.x, prerelease), 25 rows in all (counted independently of the parser), e.g. `The Feather of Justice | 2.10 | September 23, 2026 | October 29, 2026 | 36 Days`. A version stands in for the pass only because the owner confirmed in game, 2026-10-16, that the pass ends with it (it read 13d 19h at 00:47 UTC, ending 2026-10-29 with version 2.10). That claim and its evidence are one entry in `VERSION_PASSES`, keyed by source id; the event's summary repeats it (`Follows version 2.10 (The Feather of Justice). That the pass ends with the version was confirmed in game, 2026-10-16.`). Rows already ended are skipped, a row without a readable end is skipped; at capture exactly one row survives, titled `Mira Journey`, from 2026-09-23 (day precision). **Its End Date is the version's last day**, not the day it ends: the owner's timer on Europe put the end between 19:47 and 20:47 UTC on 2026-10-29, which is Asia's 04:00 reset opening the 30th. So the entry carries `endsAfterLastDay: { clock: "asia" }` and the event ends at exactly 2026-10-29T20:00Z for every server. Read as an ordinary day-precision end it would have left the calendar 9 to 24 hours early, before the pass did. Nothing is region-scoped.
**Not the history-table reader:** `fandomtable` refuses any table with a `Duration` heading and this reader requires one, so neither claims the other's page (tested across every fixture).
**Conduct:** same host as `nikki-fandom-events` (§ 11), whose recorded robots reading (the standard Fandom file) applies. The `RobotsCache` with the real User-Agent got `403` (the interstitial challenge) on 2026-10-16, so that reading and the standing `--assume-robots-on-403` decision cover it. The API answered `200` to the real User-Agent for the single fixture fetch.
**Reuse:** Endfield's wiki.gg source can copy the entry shape (title, stated link, evidence); whether it shares this reader is its own call, as wiki.gg's markup differs.
## What every one of these costs, beyond the source
Adding a game is never only a `SOURCES` entry:
- a `GameId` in `src/shared/schema.ts` — an enum value that becomes the first segment of every
completion key for that game, forever;
- a `GAMES` entry in `src/shared/games.ts` (name, short, hue, studio, `dailyTasks`), plus
`resetOffsets` / `resetHourLocal` **only where the clock is evidenced** — by the source, as Nikke's
is, or by Game-Time-Master, as Stella Sora's is (`AGENTS.md` § Event IDs);
- a fixture in `fixtures//` and a test asserting parsed output, one commit per game
(`docs/FEEDBACK.md` P1 step 4);
- the lane arriving switched off for existing readers via `adoptNewLanes`, which is automatic but
worth remembering when checking the app after a build.
## Recommended order
Steps 1 through 5 were done on 2026-08-19, and step 6's declines are now written into `AGENTS.md`
§ Scraping conduct. Every game assessed in the § P1 pass is now built or declined with its reasoning
recorded, and the two decisions it raised — Nikke's permission and Infinity Nikki's timezone — have
both been made. What is queued is the two live finds of the § 12 sweep:
1. **MementoMori** (§ 12a) — the cheapest adapter available today: a `GameId`, a `GAMES` entry, one
`SOURCES` entry, a fixture and a test, with no parser change. Seven events parse out of a
seven-row table and six of them are live.
2. **Fire Emblem Heroes** (§ 12b) — better data and a real cost: a ninth Game8 column shape, a
single-date `MM/DD/YYYY` reader, and a decision about the 180-day rule, which one legitimate
seven-month banner breaks. Two commits, parser then source, with the vocabulary widening diffed
across every fixture and snapshot as Umamusume's was.
Below that, in descending order of value: re-check `grayravens.com` if it ever puts its schedule in
a table, re-check Azur Lane if `koumakan` ever drops `ai-input=no`, and re-check `marisaimpact.com`
if it ever prints a year (§ 13). None is worth a pass until something changes upstream.
## Appendix — reproducing the checks
```bash
UA='gacha-event-tracker/1.0 (+https://gitea.lucaswinther.info/lucasw89/gacha-event-tracker)'
# conduct
curl -sS -A "$UA" https://iopwiki.com/robots.txt
curl -sS -A "$UA" -D - -o /dev/null https://stellasora.miraheze.org/wiki/Main_Page # Content-Signal?
# the pages
curl -sS -A "$UA" https://iopwiki.com/wiki/GFL2_Events > gfl2.html
curl -sS -A "$UA" https://stellasora.miraheze.org/wiki/Main_Page > stellasora.html
curl -sS -A "$UA" 'https://nikke-goddess-of-victory-international.fandom.com/api.php?action=parse&page=Event&prop=text&formatversion=2&format=json' > nikke.json
curl -sS -A "$UA" https://game8.co/games/Chaos-Zero-Nightmare/archives/559899 > czn.html
curl -sS -A "$UA" https://marisaimpact.com/robots.txt # comments only, no directive
curl -sS -A "$UA" https://marisaimpact.com/calendar90 > hi3-v90.html
# the § 12 sweep: hubs first, then the schedule page each one turned out to have.
# `--rate 30/m` keeps one request every 2s to a single host, per AGENTS.md § Scraping conduct.
for g in Black-Beacon Brawl-Stars Destiny-Rising Diablo-Immortal Epic-Seven \
fire-emblem-heroes Fire-Emblem-Shadows gundam-uce MementoMori \
Mongil-Star-Dive Pokemon-Champions Pokemon-UNITE Tower-of-Fantasy; do
args+=(-o "hub-$g.html" "https://game8.co/games/$g")
done
curl -sS --rate 30/m -A "$UA" --compressed -w '%{http_code} %{url_effective}\n' "${args[@]}"
# how stale is it? — the first question, and the cheapest
grep -o 'Last updated on:[^<]*' mmori.html
# does an existing parser read it? (offline, once a source id exists)
bun run parse czn-game8-events czn.html --json
```
Facts stated above that a future reader may want to re-check, with how they were established:
| Claim | How it was checked |
|---|---|
| game8 has no wiki hub for Nikke, GFL2, PGR, Azur Lane, Guardian Tales, Stella Sora, Aether Gazer | `HEAD https://game8.co/games/` → 404 for each, 200 for Umamusume-Pretty-Derby and Chaos-Zero-Nightmare |
| The `game8` parser already reads the CZN page | ran `game8Parser.parse` against the fetched bytes: 4 events, 2 correctly skipped for a missing start |
| `fandom.ts`, `akwiki`, `wikigg`, `bawiki`, `holodori` cannot read the new pages | `canParse` returned `false` for every combination tried |
| CI can fetch wiki.gg and Miraheze | `git log --name-only -- snapshots/` — `github-actions[bot]` commits carry `arknights-akwiki`, `endfield-wikigg`, `ba-bawiki` |
| A Fandom `robots.txt` 403 means the source is skipped, not failed | `src/ingest/robots.ts` `RobotsCache.load` — `status >= 400` other than 404/410 → `usable: false` |
| On a served address Fandom answers the runner's own client, and only `curl` was challenged | `bun -e` with `RobotsCache` + `DEFAULT_USER_AGENT` → `allowed`, `robots.txt ok`, all four sources; `curl` → `403` 8/8 with both a bare and a Chrome `User-Agent` |
| A served address is still a precondition, so that is not schedulability | run `a60eb66` (17:46 UTC, `ubuntu-latest`) → `skipped_robots` on all four Fandom sources, same Bun client, alongside nine game8 `202`s |
| All thirteen § 12 hubs exist | `GET https://game8.co/games/` → `200` for every one |
| The `game8` parser already reads the MementoMori events page | ran `game8Parser.parse` against the fetched bytes: 7 events from the 7 data rows of `Current Events \| Duration` |
| The FEH page is unreadable by shape, not by two header words | `columnLayout` finds no range column at all — `Availability` is a start and `End` is its own column, and `readStartEndTable` wants the rowspan `Start`/`End` pair |
| Game8's structural check rejects the Pokémon UNITE page | `game8Parser.canParse` → `false`; the page has 25 `a-table` matches and zero `a-header--3` |
| A hub's nav is not an index of its pages | the UNITE, Champions, Gundam and Black Beacon schedule pages exist and are linked from none of the hub navigation scanned; found by web search |
| `marisaimpact.com` states no year on any calendar | stripped tags from `/calendar89` and `/calendar90` and grepped the text for `20\d\d` — zero matches on either |
| Its calendar URL is per-version, with no stable alias | `/calendar90` is `200` and covers `Aug 20 - Oct 22`; `/valk/calendar90` — the scheme its own `og:url` uses — is `404`; the nav links only `/calendar88` and `/calendar89` |
| Its schedule is not behind an API | `GET /calendar90/__data.json` returns 144 bytes: an `ogImage` URL and nothing else |
## Redeem codes source (2026-10-16)
Genshin codes use https://game8.co/games/Genshin-Impact/archives/304759, separately from events.
One descriptive-agent robots request and one page request returned HTTP 200 during capture; no
access workaround or repeated source fetch was used. The raw page and capture metadata are pinned
as fixtures/genshin/game8-codes-2026-10-16.*, with ten independently counted current codes. The
parser reads actual code inputs/current reward rows and excludes expired programme sections; a
future-dated programme fixture variant verifies that still-current programmes are admitted.
Unannounced expiry remains null. A printed calendar date is expiresOn, never a fabricated exact
expiresAt instant. Attribution is retained on every code.
src/ingest/codes/registry.ts registers a count-only refresh source. The existing runner shares robots,
six-hour floors, conditional requests, per-host spacing and snapshot gates across event/code sources.
Event adapter schemas are unchanged. scripts/build-codes.ts builds data/codes.v1.json offline from
snapshots or fixtures and preserves observation timestamps in snapshots/codes-first-seen.json.
Code newness is per game; redemption is per local game/server account and never clears newness by
itself. No code row is synthesized into the event feed.
## Redeem codes for eight more games (2026-10-18)
Codes now come from Game8 for Star Rail (archives/410296), ZZZ (435683), Wuthering Waves (453149),
Endfield (571509), NTE (593718), P5X (531770), Chaos Zero Nightmare (557967) and Umamusume (536130),
beside Genshin.
- **Capture.** game8.co's robots.txt has no rule for our User-Agent or these paths. Each page was
fetched exactly once with the refresh User-Agent, 2s apart, all HTTP 200, and pinned as
`fixtures//game8-codes-2026-10-18.*`.
- **Parser.** The pages share Game8's copy-input markup but not their headings or columns.
`parseGame8CodeTable` reads the section under each page's own "active codes" heading
(`src/ingest/codes/registry.ts`), skips any table there that says its codes have expired
(Wuthering Waves keeps one at the top), and reads the reward from the other cells, or from the
code's own cell when they share one (Wuthering Waves, P5X).
- **Expiry stays null.** The pages print "Expires TBA" or a date range without a year, and a guessed
year would be a fabricated date.
- **Umamusume.** The page currently states "No Publicly Released Codes Yet", which is what lets its
empty read be stored (`statesNoEvents`). A changed page still fails the gate.
- **Spot-checked against the live pages:** Star Rail's six codes, WUTHERINGGIFT, Endfield's
ENDFIELDSTEAM / ENDFIELDGIFT / ENDFIELD4PC, CZN's NIGHTMARECARNIVAL and P5X's WEBSTORE1YEAR.
**Infinity Nikki is not added.** Game8 stopped updating its Nikki guides and the codes page is still
titled "All Infinity Nikki Codes (July 2025)", so its twelve "active" codes cannot be presented as
current. The other games (Arknights, Blue Archive, FGO, hololive Dreams, GFL2, Stella Sora, Nikke,
Honkai Impact 3rd, PGR, Reverse: 1999) have no Game8 codes page. Searches found only general blogs,
which are not assessed yet.
**Where to redeem** (`src/shared/redeem.ts`). Each game with a codes source shows its in-game route,
taken from its Game8 page's "How to redeem" section. An official web page gets a button only when the
page links one on the publisher's domain. Today that is Genshin, Star Rail and ZZZ on hoyoverse.com.
Chaos Zero Nightmare names an official coupon page without linking it, and no URL could be confirmed,
so it shows the in-game route only.
**Refreshing.** These are game8 pages, so the CI caveat in AGENTS.md § Scraping conduct applies: if the
runner is refused, they refresh by hand like the event pages.