Track every Niantic Wayspot (or Ingress portal) nomination you've submitted — what you wrote, the photos you attached, and whether it's Pending, Accepted, or Rejected — in one private, local web app.
Two pieces work together:
gmail_wayspot_export.py— a script that reads your own Gmail and pulls out every nomination email into a single JSON file.portal-submission-tracker.html— a standalone web app that displays, edits, and tracks that data.
Nothing here talks to Niantic directly, and nothing leaves your computer except the one-time login to your own Google account.
(Illustrative mockups with placeholder data only — no real submissions shown.)
Overview table — search, filter, sort, and status counts at a glance:
Detail view — submission text, supporting text, and both photos pulled from Gmail, plus an optional manual photo:
You need your own Google Cloud OAuth credentials. This is a Google requirement for any app that reads Gmail — there's no way around this step, but it only takes a few minutes and you only do it once.
-
Install the Python dependencies:
pip install -r requirements.txt(If you'd rather install them one by one:
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib beautifulsoup4) -
Create a Google Cloud project:
- Go to console.cloud.google.com
- Click the project dropdown at the top → New Project → give it any name (e.g. "Wayspot Tracker") → Create
-
Enable the Gmail API:
- In the search bar, type "Gmail API" → open it → click Enable
-
Set up the OAuth consent screen:
- Go to APIs & Services → OAuth consent screen
- Choose External (unless you have a Google Workspace account) → Create
- Fill in an app name, your email as the support email, and your email again under developer contact → Save and continue through the remaining steps
- Under Test users, add your own Gmail address — this lets you use the app while it's unverified
-
Create OAuth credentials:
- Go to APIs & Services → Credentials → Create Credentials → OAuth client ID
- Application type: Desktop app → Create
- Click the download icon next to the new credential → save the file
- Rename the downloaded file to exactly
credentials.jsonand put it in the same folder asgmail_wayspot_export.py
python gmail_wayspot_export.py
- The first time you run it, a browser window opens asking you to log in and approve read-only Gmail access. The script cannot send, delete, or modify anything.
- A
token.jsonfile is saved afterward so you won't have to log in again next time. - It searches for:
"Niantic Spatial Wayspot nomination received for"— new Wayspot nominations, plus their decisions"Thanks! Niantic Wayspot nomination received for"(no "Spatial") — the same, but from before Niantic's Wayfarer→Spatial rebrand, plus their decisions"Thanks! Niantic Spatial Wayspot Photo received for"— photos added to existing Wayspots, plus their decisions"Thanks! Niantic Spatial Wayspot edit suggestion received for"— title/description/location edits suggested for an existing Wayspot, plus their decisions"Thanks! Niantic Spatial Wayspot appeal received"/"...title edit appeal received for"— appeals of a rejected nomination, photo, or edit
- Note: Niantic's photo-submission confirmation emails don't include any text or the photo itself (unlike nominations, which include both) — so those entries will only have a portal name, date, and eventual status.
- Coordinates: the legacy Wayfarer emails include a
(lat, lng)pair that the newer Spatial emails don't (confirmed by checking a real Spatial email, not assumed). These are captured automatically for Wayfarer-era nominations. For everything else — Spatial nominations, photos, edits, or anything from before this feature existed — coordinates can be filled in by hand in the app. - Edit suggestions include the existing value and your suggested replacement, tagged with which field was edited (Title / Description / Location / etc).
- Appeals aren't a new row — an appeal email references the original submission by name and date, so the script finds that entry and flips its status to
Appealinstead of duplicating it. If it can't find a confident match, it adds the appeal as its own row instead of silently dropping it, with a note flagging it for manual review. ⚠️ Known limitation: the decided-appeal email subject ("Your Niantic Spatial Wayspot appeal has been decided") is a best guess — no real example was available while building this. If your appeal statuses don't update correctly after a decision comes in, open one of those emails, compare it againstparse_appeal_decision()in the script, and adjust the parsing to match the real wording.- Old and new nomination emails are merged, not duplicated. Niantic's nomination system has gone through three eras — OPR (
nominations@portals.ingress.com, the original Ingress portal review system), Wayfarer, and now Spatial/Recon — and this script merges all three: Spatial data wins when more than one exists for the same portal+date, legacy-only nominations from either older era are still included, and whichever decision email is chronologically the most recent wins the final status (sometimes a portal was accepted under an older system and later rejected under a newer one after a re-review). - OPR is the simplest era: one description paragraph (no separate "supporting text"), one photo, no coordinates. Its decision emails also use different wording than the other two ("Excellent work, Agent... eligible Portal nomination" rather than "Congratulations... accepted") — confirmed against real emails before building this, since guessing wrong here would have silently misclassified every OPR-era acceptance.
- Renamed portals are reconciled, not lost. If a nomination's title was changed via an accepted title-edit suggestion before its final decision arrived, Niantic's decision email can reference the new name — which won't match the original submission by name alone. The script builds a map of every accepted title edit and retries any decision that didn't match directly against it, so the right nomination still gets updated. If a decision genuinely can't be matched even after that (shouldn't normally happen), it's added as its own flagged row instead of silently dropped, and the script prints a clear summary either way — either "All decision emails were successfully matched" or a list of exactly which ones weren't, so you always know the full accept/reject picture is reflected somewhere.
- It prints progress as it goes, then writes
wayspot_submissions.jsonin the same folder, shaped as{ "exported_at": "...", "submissions": [...] }— the timestamp lets the tracker show when your Gmail data was actually last fetched, not just when you happened to click Import. (Older plain-array exports from before this change still import fine.) - Incremental sync: after the first run, it only fetches messages newer than the last run (tracked in a small
sync_state.jsonfile, with a 1-day safety buffer) and merges them into your existing data — so re-running weekly to catch new decisions takes seconds instead of re-scanning your whole mailbox. This applies to decision/appeal emails too, so a decision arriving for a nomination from months ago still gets applied correctly to that old entry. Deletesync_state.jsonif you ever want to force a full re-sync from scratch (e.g. after this script gets a parsing fix).
Re-run it anytime to pick up new submissions or decisions — the tracker's import step below is smart about merging updates.
| Error | Fix |
|---|---|
FileNotFoundError: credentials.json |
You haven't completed step 5 above, or the file isn't named/placed correctly. |
| Browser says "app isn't verified" | Click Advanced → Go to [app name] (unsafe). This is normal for a personal script only you use — you added yourself as a test user in step 4. |
403 or access_denied |
Make sure you added your own email under Test users in the OAuth consent screen. |
RefreshError: invalid_grant |
Your saved login expired or was revoked. The script now handles this automatically (opens a fresh login instead of crashing) — if you're on an older copy, just delete token.json and run again. This is expected to happen roughly every 7 days as long as the OAuth app stays in Google's "Testing" status, since Google expires refresh tokens for unverified apps on that schedule — not a bug you need to fix, just re-log-in when it happens. |
| No results found | Double-check the emails are actually in Gmail (not archived to a different account) and that the subject lines match — Niantic may have changed wording since this was written. |
Open portal-submission-tracker.html in any browser (just double-click it, no server needed).
Click Import JSON and select your wayspot_submissions.json. This is a merge, not a wipe:
- New portals are added
- Existing ones (matched by name + submission date) get their status/text/photos refreshed
- Your own notes and any manually-attached photos are left alone
Re-import anytime after re-running the script to bring in new decisions.
- Click any portal name to open its detail view — submission text, supporting text, and both photos (submission + supporting), each clickable for a full-size view
- Each entry is tagged Nomination, Photo (a photo added to an existing Wayspot), or Edit (a suggested title/description/location change) — sortable by that column too
- Add / Edit / Delete entries by hand
- Click any status badge for a one-click status change (Pending/Accepted/Rejected/Duplicate/Appeal), without opening the full edit form
- Parse email — paste a single confirmation email's text to auto-fill a new entry, if you'd rather not use the Python script for a one-off
- Search, filter, and sort — click any column label to sort; click the ▾ next to Type, Status, Submitted, or Last Updated to filter by a checklist of values (Type/Status) or a date range with quick presets like "Last 30 days" (Submitted/Last Updated)
- A checkbox toggles whether legacy Wayfarer-only nominations (ones with no Spatial-branded counterpart, from before Niantic's rebrand) are shown — on by default, so nothing's hidden unless you choose to
- Export CSV for a spreadsheet-friendly copy of everything
- Attach your own photo to any entry (separate from the ones pulled from Gmail)
- "Gmail data last synced" shown right under the header, using the export script's own timestamp — turns amber if it's been over 30 days
- Coordinates — auto-filled for legacy Wayfarer nominations; editable by hand for anything else, with a "View on map" link that appears once both fields are filled
- Clear all — wipes every entry (with a confirmation first), if you want to start fresh
All data is stored locally to this file/browser — nothing is sent to any server. The photo URLs point to Google's own image hosting (the same links from your emails), so viewing them does briefly contact Google's servers, same as opening the original email would.
| File | Purpose |
|---|---|
gmail_wayspot_export.py |
Reads your Gmail, writes wayspot_submissions.json |
requirements.txt |
Python dependencies for the export script |
sync_state.json |
Created automatically after your first run — tracks incremental sync progress. Delete to force a full re-sync. |
portal-submission-tracker.html |
The tracker app itself |
screenshot-overview.svg / screenshot-detail.svg |
Placeholder screenshots used in this README |
README.md |
This file |
You'll also end up with credentials.json and token.json after setup — keep those private, they're tied to your Google account.